From 134112f3f9d8f5798b278e36c846cba941f9bcf0 Mon Sep 17 00:00:00 2001 From: Colin Date: Sat, 23 Oct 2021 13:25:39 -0400 Subject: [PATCH 01/28] Add template for canvasfeaturerenderer --- plugins/canvas/.babelrc | 9 + plugins/canvas/package.json | 47 + .../src/CanvasFeatureRenderer/README.md | 33 + .../CanvasFeatureRenderer/components/Box.js | 66 ++ .../components/Chevron.js | 113 ++ .../components/FeatureGlyph.js | 111 ++ .../components/FeatureLabel.tsx | 46 + .../components/ProcessedTranscript.js | 175 +++ .../components/Segments.js | 104 ++ .../components/Subfeatures.js | 81 ++ .../components/SvgFeatureRendering.js | 420 +++++++ .../components/SvgFeatureRendering.test.js | 1041 +++++++++++++++++ .../components/SvgOverlay.tsx | 247 ++++ .../SvgFeatureRendering.test.js.snap | 585 +++++++++ .../CanvasFeatureRenderer/components/util.ts | 129 ++ .../src/CanvasFeatureRenderer/configSchema.ts | 109 ++ .../canvas/src/CanvasFeatureRenderer/index.ts | 2 + plugins/canvas/src/index.ts | 32 + plugins/canvas/tsconfig.json | 10 + plugins/canvas/tsdx.config.js | 8 + 20 files changed, 3368 insertions(+) create mode 100644 plugins/canvas/.babelrc create mode 100644 plugins/canvas/package.json create mode 100644 plugins/canvas/src/CanvasFeatureRenderer/README.md create mode 100644 plugins/canvas/src/CanvasFeatureRenderer/components/Box.js create mode 100644 plugins/canvas/src/CanvasFeatureRenderer/components/Chevron.js create mode 100644 plugins/canvas/src/CanvasFeatureRenderer/components/FeatureGlyph.js create mode 100644 plugins/canvas/src/CanvasFeatureRenderer/components/FeatureLabel.tsx create mode 100644 plugins/canvas/src/CanvasFeatureRenderer/components/ProcessedTranscript.js create mode 100644 plugins/canvas/src/CanvasFeatureRenderer/components/Segments.js create mode 100644 plugins/canvas/src/CanvasFeatureRenderer/components/Subfeatures.js create mode 100644 plugins/canvas/src/CanvasFeatureRenderer/components/SvgFeatureRendering.js create mode 100644 plugins/canvas/src/CanvasFeatureRenderer/components/SvgFeatureRendering.test.js create mode 100644 plugins/canvas/src/CanvasFeatureRenderer/components/SvgOverlay.tsx create mode 100644 plugins/canvas/src/CanvasFeatureRenderer/components/__snapshots__/SvgFeatureRendering.test.js.snap create mode 100644 plugins/canvas/src/CanvasFeatureRenderer/components/util.ts create mode 100644 plugins/canvas/src/CanvasFeatureRenderer/configSchema.ts create mode 100644 plugins/canvas/src/CanvasFeatureRenderer/index.ts create mode 100644 plugins/canvas/src/index.ts create mode 100644 plugins/canvas/tsconfig.json create mode 100644 plugins/canvas/tsdx.config.js diff --git a/plugins/canvas/.babelrc b/plugins/canvas/.babelrc new file mode 100644 index 0000000000..dde1819d9f --- /dev/null +++ b/plugins/canvas/.babelrc @@ -0,0 +1,9 @@ +{ + "presets": [ + // need this to be able to use spread operator on Set and Map + // see https://github.com/formium/tsdx/issues/376#issuecomment-566750042 + ["@babel/preset-env", { "loose": false }], + // can remove this if all .js files are converted to .ts + "@babel/preset-react" + ] +} diff --git a/plugins/canvas/package.json b/plugins/canvas/package.json new file mode 100644 index 0000000000..4bacb21655 --- /dev/null +++ b/plugins/canvas/package.json @@ -0,0 +1,47 @@ +{ + "name": "@jbrowse/plugin-canvas", + "version": "1.5.0", + "description": "JBrowse 2 plugin for canvas features", + "keywords": [ + "jbrowse", + "jbrowse2" + ], + "license": "Apache-2.0", + "homepage": "https://jbrowse.org", + "bugs": "https://github.com/GMOD/jbrowse-components/issues", + "repository": { + "type": "git", + "url": "https://github.com/GMOD/jbrowse-components.git", + "directory": "plugins/canvas" + }, + "author": "JBrowse Team", + "distMain": "dist/index.js", + "srcMain": "src/index.ts", + "main": "src/index.ts", + "distModule": "dist/plugin-canvas.esm.js", + "module": "", + "files": [ + "dist", + "src" + ], + "scripts": { + "start": "tsdx watch --verbose --noClean", + "build": "tsdx build", + "test": "cd ../..; jest plugins/canvas", + "prepublishOnly": "yarn test", + "prepack": "yarn build; yarn useDist", + "postpack": "yarn useSrc", + "useDist": "node ../../scripts/useDist.js", + "useSrc": "node ../../scripts/useSrc.js" + }, + "peerDependencies": { + "@jbrowse/core": "^1.0.0", + "mobx-react": "^6.0.0", + "mobx-state-tree": "3.14.1", + "prop-types": "^15.0.0", + "react": ">=16.8.0" + }, + "publishConfig": { + "access": "public" + } +} diff --git a/plugins/canvas/src/CanvasFeatureRenderer/README.md b/plugins/canvas/src/CanvasFeatureRenderer/README.md new file mode 100644 index 0000000000..0a4e19213c --- /dev/null +++ b/plugins/canvas/src/CanvasFeatureRenderer/README.md @@ -0,0 +1,33 @@ +# SVG Feature Renderer + +## Process + +The SVG Feature renderer has two steps: + +- **Layout**: Each feature, subfeature, and label is laid out so that they are + all allocated the proper space. First, `layOut` is called on the top-level + feature, and then that function is responsible for calling `layOut` on its + subfeatures. The default `layOut` function just calls `layOut` on all the + subfeatures. The `layOut` function can be customized by a glyph by providing a + `layOut` method on the Glyph function. This can be used to e.g. filter certain + subfeatures so they are not displayed or manipulate the position of the + subfeatures. + +- **Render**: Each glyph is a React component that returns a valid SVG element. + It positions itself using the `featureLayout` prop. Each rendered feature has its own glyph. + +## Glyphs + +- **Box**: A simple rectangle from the start to the end of a feature. + +- **Chevron**: A rectangle that is pointed/indented at the ends. It points + itself according to the feature's strand. + +- **ProcessedTranscript**: A `Segments` glyph that only lays out/renders its + subfeatures that are of type CDS, UTR, five_prime_UTR, or three_prime_UTR. + +- **Segments**: A glyph that draws a line from the start to the end of a + feature. The subfeatures are drawn on top of this line. + +- **Subfeatures**: Does not render the feature itself, but renders its + subfeatures so they are vertically offset from each other. diff --git a/plugins/canvas/src/CanvasFeatureRenderer/components/Box.js b/plugins/canvas/src/CanvasFeatureRenderer/components/Box.js new file mode 100644 index 0000000000..beacfcee48 --- /dev/null +++ b/plugins/canvas/src/CanvasFeatureRenderer/components/Box.js @@ -0,0 +1,66 @@ +import { readConfObject } from '@jbrowse/core/configuration' +import { PropTypes as CommonPropTypes } from '@jbrowse/core/util/types/mst' +import { emphasize } from '@jbrowse/core/util/color' +import { observer } from 'mobx-react' +import ReactPropTypes from 'prop-types' +import React from 'react' + +function Box(props) { + const { feature, region, config, featureLayout, selected, bpPerPx } = props + const screenWidth = (region.end - region.start) / bpPerPx + + const color1 = readConfObject(config, 'color1', { feature }) + let emphasizedColor1 + try { + emphasizedColor1 = emphasize(color1, 0.3) + } catch (error) { + emphasizedColor1 = color1 + } + const color2 = readConfObject(config, 'color2', { feature }) + + const { left, top, width, height } = featureLayout.absolute + + if (left + width < 0) { + return null + } + const leftWithinBlock = Math.max(left, 0) + const diff = leftWithinBlock - left + const widthWithinBlock = Math.max(1, Math.min(width - diff, screenWidth)) + + return ( + + ) +} + +Box.propTypes = { + feature: ReactPropTypes.shape({ + get: ReactPropTypes.func.isRequired, + id: ReactPropTypes.func.isRequired, + }).isRequired, + region: CommonPropTypes.Region.isRequired, + bpPerPx: ReactPropTypes.number.isRequired, + featureLayout: ReactPropTypes.shape({ + absolute: ReactPropTypes.shape({ + top: ReactPropTypes.number.isRequired, + left: ReactPropTypes.number.isRequired, + width: ReactPropTypes.number.isRequired, + height: ReactPropTypes.number.isRequired, + }), + }).isRequired, + selected: ReactPropTypes.bool, + config: CommonPropTypes.ConfigSchema.isRequired, +} + +Box.defaultProps = { + selected: false, +} + +export default observer(Box) diff --git a/plugins/canvas/src/CanvasFeatureRenderer/components/Chevron.js b/plugins/canvas/src/CanvasFeatureRenderer/components/Chevron.js new file mode 100644 index 0000000000..59b9f06f3f --- /dev/null +++ b/plugins/canvas/src/CanvasFeatureRenderer/components/Chevron.js @@ -0,0 +1,113 @@ +import { readConfObject } from '@jbrowse/core/configuration' +import { PropTypes as CommonPropTypes } from '@jbrowse/core/util/types/mst' +import { emphasize } from '@jbrowse/core/util/color' +import { observer } from 'mobx-react' +import ReactPropTypes from 'prop-types' +import React from 'react' +import { isUTR } from './util' + +const utrHeightFraction = 0.65 + +function Chevron(props) { + const { + feature, + bpPerPx, + region, + config, + featureLayout, + selected, + reversed, + } = props + + const screenWidth = (region.end - region.start) / bpPerPx + const width = Math.max(featureLayout.absolute.width, 1) + const { left } = featureLayout.absolute + let { top, height } = featureLayout.absolute + if (isUTR(feature)) { + top += ((1 - utrHeightFraction) / 2) * height + height *= utrHeightFraction + } + + const strand = feature.get('strand') + const direction = strand * (reversed ? -1 : 1) + const color = isUTR(feature) + ? readConfObject(config, 'color3', { feature }) + : readConfObject(config, 'color1', { feature }) + let emphasizedColor + try { + emphasizedColor = emphasize(color, 0.3) + } catch (error) { + emphasizedColor = color + } + const color2 = readConfObject(config, 'color2', { feature }) + + if (left + width < 0) { + return null + } + const leftWithinBlock = Math.max(left, 0) + const diff = leftWithinBlock - left + const widthWithinBlock = Math.max(1, Math.min(width - diff, screenWidth)) + + return ( + <> + + {direction < 0 && diff === 0 ? ( + + ) : null} + {direction > 0 && leftWithinBlock + widthWithinBlock < screenWidth ? ( + + ) : null} + + ) +} + +Chevron.propTypes = { + feature: ReactPropTypes.shape({ + id: ReactPropTypes.func.isRequired, + get: ReactPropTypes.func.isRequired, + }).isRequired, + region: CommonPropTypes.Region.isRequired, + bpPerPx: ReactPropTypes.number.isRequired, + featureLayout: ReactPropTypes.shape({ + absolute: ReactPropTypes.shape({ + top: ReactPropTypes.number.isRequired, + left: ReactPropTypes.number.isRequired, + width: ReactPropTypes.number.isRequired, + height: ReactPropTypes.number.isRequired, + }), + }).isRequired, + selected: ReactPropTypes.bool, + config: CommonPropTypes.ConfigSchema.isRequired, + reversed: ReactPropTypes.bool, +} + +Chevron.defaultProps = { + selected: false, + reversed: false, +} + +export default observer(Chevron) diff --git a/plugins/canvas/src/CanvasFeatureRenderer/components/FeatureGlyph.js b/plugins/canvas/src/CanvasFeatureRenderer/components/FeatureGlyph.js new file mode 100644 index 0000000000..e23bf6cd4c --- /dev/null +++ b/plugins/canvas/src/CanvasFeatureRenderer/components/FeatureGlyph.js @@ -0,0 +1,111 @@ +import { readConfObject } from '@jbrowse/core/configuration' +import { PropTypes as CommonPropTypes } from '@jbrowse/core/util/types/mst' +import { observer } from 'mobx-react' +import PropTypes from 'prop-types' +import React from 'react' +import FeatureLabel from './FeatureLabel' + +function FeatureGlyph(props) { + const { + feature, + rootLayout, + selected, + config, + name, + shouldShowName, + description, + shouldShowDescription, + fontHeight, + allowedWidthExpansion, + reversed, + } = props + + const featureLayout = rootLayout.getSubRecord(String(feature.id())) + const { GlyphComponent } = featureLayout.data + + const glyphComponents = [ + , + ] + + if (shouldShowName) { + glyphComponents.push( + , + ) + } + + if (shouldShowDescription) { + glyphComponents.push( + , + ) + } + + return {glyphComponents} +} + +FeatureGlyph.propTypes = { + feature: PropTypes.shape({ + id: PropTypes.func.isRequired, + get: PropTypes.func.isRequired, + }).isRequired, + layout: PropTypes.shape({ + addRect: PropTypes.func.isRequired, + getTotalHeight: PropTypes.func.isRequired, + }).isRequired, + rootLayout: PropTypes.shape({ + addChild: PropTypes.func.isRequired, + getSubRecord: PropTypes.func.isRequired, + }).isRequired, + region: CommonPropTypes.Region.isRequired, + bpPerPx: PropTypes.number.isRequired, + reversed: PropTypes.bool, + selected: PropTypes.bool, + config: CommonPropTypes.ConfigSchema.isRequired, + name: PropTypes.string, + shouldShowName: PropTypes.bool, + description: PropTypes.string, + shouldShowDescription: PropTypes.bool, + fontHeight: PropTypes.number, + allowedWidthExpansion: PropTypes.number, + movedDuringLastMouseDown: PropTypes.bool.isRequired, +} + +FeatureGlyph.defaultProps = { + reversed: false, + selected: false, + name: '', + shouldShowName: false, + description: '', + shouldShowDescription: false, + fontHeight: undefined, + allowedWidthExpansion: undefined, +} + +export default observer(FeatureGlyph) diff --git a/plugins/canvas/src/CanvasFeatureRenderer/components/FeatureLabel.tsx b/plugins/canvas/src/CanvasFeatureRenderer/components/FeatureLabel.tsx new file mode 100644 index 0000000000..f18fab8bf7 --- /dev/null +++ b/plugins/canvas/src/CanvasFeatureRenderer/components/FeatureLabel.tsx @@ -0,0 +1,46 @@ +import React from 'react' +import { measureText } from '@jbrowse/core/util' + +export default function Label(props: { + text: string + x: number + y: number + color?: string + fontHeight?: number + featureWidth?: number + allowedWidthExpansion?: number + reversed?: boolean + fontWidthScaleFactor?: number +}) { + const { + text, + x, + y, + color = 'black', + fontHeight = 13, + featureWidth = 0, + reversed, + allowedWidthExpansion, + fontWidthScaleFactor = 0.6, + } = props + + const fontWidth = fontHeight * fontWidthScaleFactor + const totalWidth = + featureWidth && allowedWidthExpansion + ? featureWidth + allowedWidthExpansion + : Infinity + + const measuredTextWidth = measureText(text, fontHeight) + + return ( + + {measuredTextWidth > totalWidth + ? `${text.slice(0, totalWidth / fontWidth)}...` + : text} + + ) +} diff --git a/plugins/canvas/src/CanvasFeatureRenderer/components/ProcessedTranscript.js b/plugins/canvas/src/CanvasFeatureRenderer/components/ProcessedTranscript.js new file mode 100644 index 0000000000..45d251bd47 --- /dev/null +++ b/plugins/canvas/src/CanvasFeatureRenderer/components/ProcessedTranscript.js @@ -0,0 +1,175 @@ +import { observer } from 'mobx-react' +import React from 'react' +import SimpleFeature from '@jbrowse/core/util/simpleFeature' +import { readConfObject } from '@jbrowse/core/configuration' +import Segments from './Segments' +import { layOutFeature, layOutSubfeatures } from './util' + +function ProcessedTranscript(props) { + // eslint-disable-next-line react/prop-types + const { feature, config } = props + const subfeatures = getSubparts(feature, config) + + // we manually compute some subfeatures, so pass these separately + return +} + +// make a function that will filter features features according to the +// subParts conf var +function makeSubpartsFilter(confKey = 'subParts', config) { + let filter = readConfObject(config, confKey) + + if (typeof filter == 'string') { + // convert to array + filter = filter.split(/\s*,\s*/) + } + + if (Array.isArray(filter)) { + const typeNames = filter.map(typeName => typeName.toLowerCase()) + return feature => { + return typeNames.includes(feature.get('type').toLowerCase()) + } + } + if (typeof filter === 'function') { + return filter + } + return () => true +} + +function filterSubpart(feature, config) { + return makeSubpartsFilter('subParts', config)(feature) +} + +function isUTR(feature) { + return /(\bUTR|_UTR|untranslated[_\s]region)\b/.test( + feature.get('type') || '', + ) +} + +function makeUTRs(parent, subs) { + // based on Lincoln's UTR-making code in Bio::Graphics::Glyph::processed_transcript + const subparts = [...subs] + + let codeStart = Infinity + let codeEnd = -Infinity + + let haveLeftUTR + let haveRightUTR + + // gather exons, find coding start and end, and look for UTRs + const exons = [] + for (let i = 0; i < subparts.length; i++) { + const type = subparts[i].get('type') + if (/^cds/i.test(type)) { + if (codeStart > subparts[i].get('start')) { + codeStart = subparts[i].get('start') + } + if (codeEnd < subparts[i].get('end')) { + codeEnd = subparts[i].get('end') + } + } else if (/exon/i.test(type)) { + exons.push(subparts[i]) + } else if (isUTR(subparts[i])) { + haveLeftUTR = subparts[i].get('start') === parent.get('start') + haveRightUTR = subparts[i].get('end') === parent.get('end') + } + } + + // bail if we don't have exons and CDS + if (!(exons.length && codeStart < Infinity && codeEnd > -Infinity)) { + return subparts + } + + // make sure the exons are sorted by coord + exons.sort((a, b) => a.get('start') - b.get('start')) + + const strand = parent.get('strand') + + // make the left-hand UTRs + let start + let end + if (!haveLeftUTR) { + for (let i = 0; i < exons.length; i++) { + start = exons[i].get('start') + if (start >= codeStart) { + break + } + end = codeStart > exons[i].get('end') ? exons[i].get('end') : codeStart + const type = strand >= 0 ? 'five_prime_UTR' : 'three_prime_UTR' + subparts.unshift( + new SimpleFeature({ + parent, + id: `${parent.id()}_${type}_${i}`, + data: { start, end, strand, type }, + }), + ) + } + } + + // make the right-hand UTRs + if (!haveRightUTR) { + for (let i = exons.length - 1; i >= 0; i--) { + end = exons[i].get('end') + if (end <= codeEnd) { + break + } + + start = codeEnd < exons[i].get('start') ? exons[i].get('start') : codeEnd + const type = strand >= 0 ? 'three_prime_UTR' : 'five_prime_UTR' + subparts.push( + new SimpleFeature({ + parent, + id: `${parent.id()}_${type}_${i}`, + data: { start, end, strand, type }, + }), + ) + } + } + + return subparts +} + +function getSubparts(f, config) { + let c = f.get('subfeatures') + if (!c || !c.length) { + return [] + } + const hasUTRs = !!c.find(child => isUTR(child)) + const isTranscript = ['mRNA', 'transcript'].includes(f.get('type')) + const impliedUTRs = !hasUTRs && isTranscript + + // if we think we should use impliedUTRs, or it is specifically in the + // config, then makeUTRs + if (impliedUTRs || readConfObject(config, 'impliedUTRs')) { + c = makeUTRs(f, c) + } + + return c.filter(element => filterSubpart(element, config)) +} + +ProcessedTranscript.layOut = ({ + layout, + feature, + bpPerPx, + reversed, + config, +}) => { + const subLayout = layOutFeature({ + layout, + feature, + bpPerPx, + reversed, + config, + }) + const subfeatures = getSubparts(feature, config) + layOutSubfeatures({ + layout: subLayout, + subfeatures, + bpPerPx, + reversed, + config, + }) + return subLayout +} + +export default observer(ProcessedTranscript) diff --git a/plugins/canvas/src/CanvasFeatureRenderer/components/Segments.js b/plugins/canvas/src/CanvasFeatureRenderer/components/Segments.js new file mode 100644 index 0000000000..8a87a412b5 --- /dev/null +++ b/plugins/canvas/src/CanvasFeatureRenderer/components/Segments.js @@ -0,0 +1,104 @@ +import { readConfObject } from '@jbrowse/core/configuration' +import { PropTypes as CommonPropTypes } from '@jbrowse/core/util/types/mst' +import { emphasize } from '@jbrowse/core/util/color' +import { observer } from 'mobx-react' +import PropTypes from 'prop-types' +import React from 'react' + +function Segments(props) { + const { + feature, + featureLayout, + selected, + config, + reversed, + // some subfeatures may be computed e.g. makeUTRs, + // so these are passed as a prop + // eslint-disable-next-line react/prop-types + subfeatures: subfeaturesProp, + } = props + + const subfeatures = subfeaturesProp || feature.get('subfeatures') + const color2 = readConfObject(config, 'color2', { feature }) + let emphasizedColor2 + try { + emphasizedColor2 = emphasize(color2, 0.3) + } catch (error) { + emphasizedColor2 = color2 + } + const { left, top, width, height } = featureLayout.absolute + const points = [ + [left, top + height / 2], + [left + width, top + height / 2], + ] + const strand = feature.get('strand') + if (strand) { + points.push( + [left + width - height / 4, top + height / 4], + [left + width - height / 4, top + 3 * (height / 4)], + [left + width, top + height / 2], + ) + } + + return ( + <> + 0)) + ? `rotate(180,${left + width / 2},${top + height / 2})` + : undefined + } + points={points} + stroke={selected ? emphasizedColor2 : color2} + /> + { + // eslint-disable-next-line react/prop-types + subfeatures.map(subfeature => { + const subfeatureId = String(subfeature.id()) + const subfeatureLayout = featureLayout.getSubRecord(subfeatureId) + // This subfeature got filtered out + if (!subfeatureLayout) { + return null + } + const { GlyphComponent } = subfeatureLayout.data + return ( + + ) + }) + } + + ) +} + +Segments.propTypes = { + feature: PropTypes.shape({ + id: PropTypes.func.isRequired, + get: PropTypes.func.isRequired, + }).isRequired, + featureLayout: PropTypes.shape({ + absolute: PropTypes.shape({ + top: PropTypes.number.isRequired, + left: PropTypes.number.isRequired, + width: PropTypes.number.isRequired, + height: PropTypes.number.isRequired, + }), + getSubRecord: PropTypes.func.isRequired, + }).isRequired, + selected: PropTypes.bool, + config: CommonPropTypes.ConfigSchema.isRequired, + reversed: PropTypes.bool, +} + +Segments.defaultProps = { + selected: false, + reversed: false, +} + +export default observer(Segments) diff --git a/plugins/canvas/src/CanvasFeatureRenderer/components/Subfeatures.js b/plugins/canvas/src/CanvasFeatureRenderer/components/Subfeatures.js new file mode 100644 index 0000000000..deadbab4c8 --- /dev/null +++ b/plugins/canvas/src/CanvasFeatureRenderer/components/Subfeatures.js @@ -0,0 +1,81 @@ +import { readConfObject } from '@jbrowse/core/configuration' +import { observer } from 'mobx-react' +import PropTypes from 'prop-types' +import React from 'react' +import { chooseGlyphComponent, layOut, layOutFeature } from './util' + +function Subfeatures(props) { + const { feature, featureLayout, selected } = props + + return ( + <> + {feature.get('subfeatures').map(subfeature => { + const subfeatureId = String(subfeature.id()) + const subfeatureLayout = featureLayout.getSubRecord(subfeatureId) + const { GlyphComponent } = subfeatureLayout.data + return ( + + ) + })} + + ) +} + +Subfeatures.propTypes = { + feature: PropTypes.shape({ get: PropTypes.func.isRequired }).isRequired, + featureLayout: PropTypes.shape({ + getSubRecord: PropTypes.func.isRequired, + }).isRequired, + selected: PropTypes.bool, + reversed: PropTypes.bool, +} + +Subfeatures.defaultProps = { + selected: false, + reversed: false, +} + +Subfeatures.layOut = ({ layout, feature, bpPerPx, reversed, config }) => { + const subLayout = layOutFeature({ + layout, + feature, + bpPerPx, + reversed, + config, + }) + const displayMode = readConfObject(config, 'displayMode') + if (displayMode !== 'reducedRepresentation') { + const subfeatures = feature.get('subfeatures') || [] + let topOffset = 0 + subfeatures.forEach(subfeature => { + const SubfeatureGlyphComponent = chooseGlyphComponent(subfeature) + const subfeatureHeight = readConfObject(config, 'height', { + feature: subfeature, + }) + + const subSubLayout = (SubfeatureGlyphComponent.layOut || layOut)({ + layout: subLayout, + feature: subfeature, + bpPerPx, + reversed, + config, + }) + subSubLayout.move(0, topOffset) + topOffset += + displayMode === 'collapse' + ? 0 + : (displayMode === 'compact' + ? subfeatureHeight / 3 + : subfeatureHeight) + 2 + }) + } + return subLayout +} + +export default observer(Subfeatures) diff --git a/plugins/canvas/src/CanvasFeatureRenderer/components/SvgFeatureRendering.js b/plugins/canvas/src/CanvasFeatureRenderer/components/SvgFeatureRendering.js new file mode 100644 index 0000000000..25c50e70e4 --- /dev/null +++ b/plugins/canvas/src/CanvasFeatureRenderer/components/SvgFeatureRendering.js @@ -0,0 +1,420 @@ +import { readConfObject } from '@jbrowse/core/configuration' +import { PropTypes as CommonPropTypes } from '@jbrowse/core/util/types/mst' +import { bpToPx, measureText } from '@jbrowse/core/util' +import SceneGraph from '@jbrowse/core/util/layouts/SceneGraph' +import { observer } from 'mobx-react' +import ReactPropTypes from 'prop-types' +import React, { useEffect, useRef, useState, useCallback } from 'react' +import FeatureGlyph from './FeatureGlyph' +import SvgOverlay from './SvgOverlay' +import { chooseGlyphComponent, layOut } from './util' + +const renderingStyle = { + position: 'relative', +} + +// used to make features have a little padding for their labels +const nameWidthPadding = 2 +const textVerticalPadding = 2 + +// used so that user can click-away-from-feature below the laid out features +// (issue #1248) +const svgHeightPadding = 100 + +function RenderedFeatureGlyph(props) { + const { feature, bpPerPx, region, config, displayMode, layout } = props + const { reversed } = region + const start = feature.get(reversed ? 'end' : 'start') + const startPx = bpToPx(start, region, bpPerPx) + const labelsAllowed = displayMode !== 'compact' && displayMode !== 'collapsed' + + const rootLayout = new SceneGraph('root', 0, 0, 0, 0) + const GlyphComponent = chooseGlyphComponent(feature) + const featureLayout = (GlyphComponent.layOut || layOut)({ + layout: rootLayout, + feature, + bpPerPx, + reversed, + config, + }) + let shouldShowName + let shouldShowDescription + let name + let description + let fontHeight + let expansion + if (labelsAllowed) { + const showLabels = readConfObject(config, 'showLabels') + const showDescriptions = readConfObject(config, 'showDescriptions') + fontHeight = readConfObject(config, ['labels', 'fontSize'], { feature }) + expansion = readConfObject(config, 'maxFeatureGlyphExpansion') || 0 + name = readConfObject(config, ['labels', 'name'], { feature }) || '' + shouldShowName = /\S/.test(name) && showLabels + + description = + readConfObject(config, ['labels', 'description'], { feature }) || '' + shouldShowDescription = + /\S/.test(description) && showLabels && showDescriptions + + let nameWidth = 0 + if (shouldShowName) { + nameWidth = + Math.round( + Math.min(measureText(name, fontHeight), rootLayout.width + expansion), + ) + nameWidthPadding + rootLayout.addChild( + 'nameLabel', + 0, + featureLayout.bottom + textVerticalPadding, + nameWidth, + fontHeight, + ) + } + + let descriptionWidth = 0 + if (shouldShowDescription) { + const aboveLayout = shouldShowName + ? rootLayout.getSubRecord('nameLabel') + : featureLayout + descriptionWidth = + Math.round( + Math.min( + measureText(description, fontHeight), + rootLayout.width + expansion, + ), + ) + nameWidthPadding + rootLayout.addChild( + 'descriptionLabel', + 0, + aboveLayout.bottom + textVerticalPadding, + descriptionWidth, + fontHeight, + ) + } + } + + const topPx = layout.addRect( + feature.id(), + feature.get('start'), + feature.get('start') + rootLayout.width * bpPerPx, + rootLayout.height, + ) + if (topPx === null) { + return null + } + rootLayout.move(startPx, topPx) + + return ( + + ) +} + +RenderedFeatureGlyph.propTypes = { + layout: ReactPropTypes.shape({ + addRect: ReactPropTypes.func.isRequired, + getTotalHeight: ReactPropTypes.func.isRequired, + }).isRequired, + + displayMode: ReactPropTypes.string.isRequired, + region: CommonPropTypes.Region.isRequired, + bpPerPx: ReactPropTypes.number.isRequired, + feature: ReactPropTypes.shape({ + id: ReactPropTypes.func.isRequired, + get: ReactPropTypes.func.isRequired, + }).isRequired, + config: CommonPropTypes.ConfigSchema.isRequired, +} + +const RenderedFeatures = observer(props => { + const { features } = props + const featuresRendered = [] + for (const feature of features.values()) { + featuresRendered.push( + , + ) + } + return <>{featuresRendered} +}) +RenderedFeatures.propTypes = { + features: ReactPropTypes.oneOfType([ + ReactPropTypes.instanceOf(Map), + ReactPropTypes.arrayOf(ReactPropTypes.shape()), + ]), + layout: ReactPropTypes.shape({ + addRect: ReactPropTypes.func.isRequired, + getTotalHeight: ReactPropTypes.func.isRequired, + }).isRequired, +} + +RenderedFeatures.defaultProps = { + features: [], +} + +function SvgFeatureRendering(props) { + const { + layout, + blockKey, + regions, + bpPerPx, + features, + config, + displayModel, + exportSVG, + } = props + const [region] = regions || [] + const width = (region.end - region.start) / bpPerPx + const displayMode = readConfObject(config, 'displayMode') + + const ref = useRef() + const [mouseIsDown, setMouseIsDown] = useState(false) + const [movedDuringLastMouseDown, setMovedDuringLastMouseDown] = + useState(false) + const [height, setHeight] = useState(0) + const { + onMouseOut, + onMouseDown, + onMouseLeave, + onMouseEnter, + onMouseOver, + onMouseMove, + onMouseUp, + onClick, + } = props + + const mouseDown = useCallback( + event => { + setMouseIsDown(true) + setMovedDuringLastMouseDown(false) + const handler = onMouseDown + if (!handler) { + return undefined + } + return handler(event) + }, + [onMouseDown], + ) + + const mouseUp = useCallback( + event => { + setMouseIsDown(false) + const handler = onMouseUp + if (!handler) { + return undefined + } + return handler(event) + }, + [onMouseUp], + ) + + const mouseEnter = useCallback( + event => { + const handler = onMouseEnter + if (!handler) { + return undefined + } + return handler(event) + }, + [onMouseEnter], + ) + + const mouseLeave = useCallback( + event => { + const handler = onMouseLeave + if (!handler) { + return undefined + } + return handler(event) + }, + [onMouseLeave], + ) + + const mouseOver = useCallback( + event => { + const handler = onMouseOver + if (!handler) { + return undefined + } + return handler(event) + }, + [onMouseOver], + ) + + const mouseOut = useCallback( + event => { + const handler = onMouseOut + if (!handler) { + return undefined + } + return handler(event) + }, + [onMouseOut], + ) + + const mouseMove = useCallback( + event => { + if (mouseIsDown) { + setMovedDuringLastMouseDown(true) + } + let offsetX = 0 + let offsetY = 0 + if (ref.current) { + offsetX = ref.current.getBoundingClientRect().left + offsetY = ref.current.getBoundingClientRect().top + } + offsetX = event.clientX - offsetX + offsetY = event.clientY - offsetY + const px = region.reversed ? width - offsetX : offsetX + const clientBp = region.start + bpPerPx * px + + const featureIdCurrentlyUnderMouse = displayModel.getFeatureOverlapping( + blockKey, + clientBp, + offsetY, + ) + + if (onMouseMove) { + onMouseMove(event, featureIdCurrentlyUnderMouse) + } + }, + [ + blockKey, + bpPerPx, + mouseIsDown, + onMouseMove, + region.reversed, + region.start, + displayModel, + width, + ], + ) + + const click = useCallback( + event => { + // don't select a feature if we are clicking and dragging + if (movedDuringLastMouseDown) { + return + } + if (onClick) { + onClick(event) + } + }, + [movedDuringLastMouseDown, onClick], + ) + + useEffect(() => { + setHeight(layout.getTotalHeight()) + }, [layout]) + + if (exportSVG) { + return ( + + ) + } + return ( +
+ + + + +
+ ) +} + +SvgFeatureRendering.propTypes = { + layout: ReactPropTypes.shape({ + addRect: ReactPropTypes.func.isRequired, + getTotalHeight: ReactPropTypes.func.isRequired, + }).isRequired, + + regions: ReactPropTypes.arrayOf(CommonPropTypes.Region).isRequired, + bpPerPx: ReactPropTypes.number.isRequired, + features: ReactPropTypes.oneOfType([ + ReactPropTypes.instanceOf(Map), + ReactPropTypes.arrayOf(ReactPropTypes.shape()), + ]), + config: CommonPropTypes.ConfigSchema.isRequired, + displayModel: ReactPropTypes.shape({ + configuration: ReactPropTypes.shape({}), + getFeatureOverlapping: ReactPropTypes.func, + selectedFeatureId: ReactPropTypes.string, + featureIdUnderMouse: ReactPropTypes.string, + }), + + onMouseDown: ReactPropTypes.func, + onMouseUp: ReactPropTypes.func, + onMouseEnter: ReactPropTypes.func, + onMouseLeave: ReactPropTypes.func, + onMouseOver: ReactPropTypes.func, + onMouseOut: ReactPropTypes.func, + onMouseMove: ReactPropTypes.func, + onClick: ReactPropTypes.func, + onContextMenu: ReactPropTypes.func, + onFeatureClick: ReactPropTypes.func, + onFeatureContextMenu: ReactPropTypes.func, + blockKey: ReactPropTypes.string, + exportSVG: ReactPropTypes.shape({}), +} + +SvgFeatureRendering.defaultProps = { + displayModel: {}, + exportSVG: undefined, + + features: new Map(), + blockKey: undefined, + + onMouseDown: undefined, + onMouseUp: undefined, + onMouseEnter: undefined, + onMouseLeave: undefined, + onMouseOver: undefined, + onMouseOut: undefined, + onMouseMove: undefined, + onClick: undefined, + onContextMenu: undefined, + onFeatureClick: undefined, + onFeatureContextMenu: undefined, +} + +export default observer(SvgFeatureRendering) diff --git a/plugins/canvas/src/CanvasFeatureRenderer/components/SvgFeatureRendering.test.js b/plugins/canvas/src/CanvasFeatureRenderer/components/SvgFeatureRendering.test.js new file mode 100644 index 0000000000..76cee996e4 --- /dev/null +++ b/plugins/canvas/src/CanvasFeatureRenderer/components/SvgFeatureRendering.test.js @@ -0,0 +1,1041 @@ +import GranularRectLayout from '@jbrowse/core/util/layouts/GranularRectLayout' +import PrecomputedLayout from '@jbrowse/core/util/layouts/PrecomputedLayout' +import SimpleFeature from '@jbrowse/core/util/simpleFeature' +import React from 'react' +import { render } from '@testing-library/react' +import SvgRendererConfigSchema from '../configSchema' +import Rendering from './SvgFeatureRendering' +import SvgOverlay from './SvgOverlay' + +import '@testing-library/jest-dom/extend-expect' + +test('no features', () => { + const { container } = render( + , + ) + + expect(container.firstChild).toMatchSnapshot() +}) + +test('one feature', () => { + const { container } = render( + , + ) + + expect(container.firstChild).toMatchSnapshot() +}) + +test('one feature (compact mode)', () => { + const config = SvgRendererConfigSchema.create({ displayMode: 'compact' }) + + const { container } = render( + , + ) + + // reducedRepresentation of the transcript is just a box + expect(container.firstChild).toMatchSnapshot() +}) + +test('processed transcript (reducedRepresentation mode)', () => { + const config = SvgRendererConfigSchema.create({ + displayMode: 'reducedRepresentation', + }) + const { container } = render( + , + ) + + expect(container.firstChild).toMatchSnapshot() +}) + +test('processed transcript', () => { + const { container } = render( + , + ) + + expect(container.firstChild).toMatchSnapshot() +}) + +test('processed transcript (exons + impliedUTR)', () => { + const { container } = render( + , + ) + + // finds that the color3 is outputted for impliedUTRs + expect(container).toContainHTML('#357089') + + expect(container.firstChild).toMatchSnapshot() +}) + +// hacks existence of getFeatureByID +test('svg selected', () => { + const { container } = render( + + { + return [0, 0, 10, 10] + }, + featureIdUnderMouse: 'one', + selectedFeatureId: 'one', + }} + config={SvgRendererConfigSchema.create({})} + bpPerPx={3} + /> + , + ) + + expect(container.firstChild).toMatchSnapshot() +}) diff --git a/plugins/canvas/src/CanvasFeatureRenderer/components/SvgOverlay.tsx b/plugins/canvas/src/CanvasFeatureRenderer/components/SvgOverlay.tsx new file mode 100644 index 0000000000..469d2f7ecf --- /dev/null +++ b/plugins/canvas/src/CanvasFeatureRenderer/components/SvgOverlay.tsx @@ -0,0 +1,247 @@ +import { bpSpanPx } from '@jbrowse/core/util' +import SimpleFeature from '@jbrowse/core/util/simpleFeature' +import { Region } from '@jbrowse/core/util/types' +import { observer } from 'mobx-react' +import React from 'react' + +type LayoutRecord = [number, number, number, number] +interface SvgOverlayProps { + region: Region + displayModel: { + getFeatureByID: (arg0: string, arg1: string) => LayoutRecord + selectedFeatureId?: string + featureIdUnderMouse?: string + contextMenuFeature?: SimpleFeature + } + bpPerPx: number + blockKey: string + movedDuringLastMouseDown: boolean + onFeatureMouseDown?( + event: React.MouseEvent, + featureId: string, + ): {} + onFeatureMouseEnter?( + event: React.MouseEvent, + featureId: string, + ): {} + onFeatureMouseOut?( + event: + | React.MouseEvent + | React.FocusEvent, + featureId: string, + ): {} + onFeatureMouseOver?( + event: + | React.MouseEvent + | React.FocusEvent, + featureId: string, + ): {} + onFeatureMouseUp?( + event: React.MouseEvent, + featureId: string, + ): {} + onFeatureMouseLeave?( + event: React.MouseEvent, + featureId: string, + ): {} + onFeatureMouseMove?( + event: React.MouseEvent, + featureId: string, + ): {} + // synthesized from mouseup and mousedown + onFeatureClick?( + event: React.MouseEvent, + featureId: string, + ): {} + onFeatureContextMenu?( + event: React.MouseEvent, + featureId: string, + ): {} +} + +interface OverlayRectProps extends React.SVGProps { + rect?: LayoutRecord + region: Region + bpPerPx: number +} + +function OverlayRect({ + rect, + region, + bpPerPx, + ...rectProps +}: OverlayRectProps) { + if (!rect) { + return null + } + const [leftBp, topPx, rightBp, bottomPx] = rect + const [leftPx, rightPx] = bpSpanPx(leftBp, rightBp, region, bpPerPx) + const rectTop = Math.round(topPx) + const screenWidth = (region.end - region.start) / bpPerPx + const rectHeight = Math.round(bottomPx - topPx) + const width = rightPx - leftPx + + if (leftPx + width < 0) { + return null + } + const leftWithinBlock = Math.max(leftPx, 0) + const diff = leftWithinBlock - leftPx + const widthWithinBlock = Math.max(1, Math.min(width - diff, screenWidth)) + + return ( + + ) +} + +function SvgOverlay({ + displayModel, + blockKey, + region, + bpPerPx, + movedDuringLastMouseDown, + ...handlers +}: SvgOverlayProps) { + const { selectedFeatureId, featureIdUnderMouse, contextMenuFeature } = + displayModel + + const mouseoverFeatureId = featureIdUnderMouse || contextMenuFeature?.id() + + function onFeatureMouseDown( + event: React.MouseEvent, + ) { + const { onFeatureMouseDown: handler } = handlers + if (!(handler && mouseoverFeatureId)) { + return undefined + } + return handler(event, mouseoverFeatureId) + } + + function onFeatureMouseEnter( + event: React.MouseEvent, + ) { + const { onFeatureMouseEnter: handler } = handlers + if (!(handler && mouseoverFeatureId)) { + return undefined + } + return handler(event, mouseoverFeatureId) + } + + function onFeatureMouseOut( + event: + | React.MouseEvent + | React.FocusEvent, + ) { + const { onFeatureMouseOut: handler } = handlers + if (!(handler && mouseoverFeatureId)) { + return undefined + } + return handler(event, mouseoverFeatureId) + } + + function onFeatureMouseOver( + event: + | React.MouseEvent + | React.FocusEvent, + ) { + const { onFeatureMouseOver: handler } = handlers + if (!(handler && mouseoverFeatureId)) { + return undefined + } + return handler(event, mouseoverFeatureId) + } + + function onFeatureMouseUp( + event: React.MouseEvent, + ) { + const { onFeatureMouseUp: handler } = handlers + if (!(handler && mouseoverFeatureId)) { + return undefined + } + return handler(event, mouseoverFeatureId) + } + + function onFeatureMouseLeave( + event: React.MouseEvent, + ) { + const { onFeatureMouseLeave: handler } = handlers + if (!(handler && mouseoverFeatureId)) { + return undefined + } + return handler(event, mouseoverFeatureId) + } + + function onFeatureMouseMove( + event: React.MouseEvent, + ) { + const { onFeatureMouseMove: handler } = handlers + if (!(handler && mouseoverFeatureId)) { + return undefined + } + return handler(event, mouseoverFeatureId) + } + + function onFeatureClick(event: React.MouseEvent) { + if (movedDuringLastMouseDown) { + return undefined + } + const { onFeatureClick: handler } = handlers + if (!(handler && mouseoverFeatureId)) { + return undefined + } + event.stopPropagation() + return handler(event, mouseoverFeatureId) + } + + function onFeatureContextMenu( + event: React.MouseEvent, + ) { + const { onFeatureContextMenu: handler } = handlers + if (!(handler && mouseoverFeatureId)) { + return undefined + } + return handler(event, mouseoverFeatureId) + } + + return ( + <> + {mouseoverFeatureId ? ( + + ) : null} + {selectedFeatureId ? ( + + ) : null} + + ) +} + +export default observer(SvgOverlay) diff --git a/plugins/canvas/src/CanvasFeatureRenderer/components/__snapshots__/SvgFeatureRendering.test.js.snap b/plugins/canvas/src/CanvasFeatureRenderer/components/__snapshots__/SvgFeatureRendering.test.js.snap new file mode 100644 index 0000000000..9bb0eb1feb --- /dev/null +++ b/plugins/canvas/src/CanvasFeatureRenderer/components/__snapshots__/SvgFeatureRendering.test.js.snap @@ -0,0 +1,585 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`no features 1`] = ` +
+ +
+`; + +exports[`one feature (compact mode) 1`] = ` +
+ + + + + + + + + + + + + + + + + + + +
+`; + +exports[`one feature 1`] = ` +
+ + + + + +
+`; + +exports[`processed transcript (exons + impliedUTR) 1`] = ` +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + LACTBL1 + + + lactamase beta like 1 + + + +
+`; + +exports[`processed transcript (reducedRepresentation mode) 1`] = ` +
+ + + + + +
+`; + +exports[`processed transcript 1`] = ` +
+ + + + + + + + + + + + + + + + + + + au9.g1002.t1 + + + +
+`; + +exports[`svg selected 1`] = ` + + + + +`; diff --git a/plugins/canvas/src/CanvasFeatureRenderer/components/util.ts b/plugins/canvas/src/CanvasFeatureRenderer/components/util.ts new file mode 100644 index 0000000000..c495a583db --- /dev/null +++ b/plugins/canvas/src/CanvasFeatureRenderer/components/util.ts @@ -0,0 +1,129 @@ +import React from 'react' +import { readConfObject } from '@jbrowse/core/configuration' +import SceneGraph from '@jbrowse/core/util/layouts/SceneGraph' +import { Feature } from '@jbrowse/core/util/simpleFeature' +import { AnyConfigurationModel } from '@jbrowse/core/configuration/configurationSchema' +import Box from './Box' +import Chevron from './Chevron' +import ProcessedTranscript from './ProcessedTranscript' +import Segments from './Segments' +import Subfeatures from './Subfeatures' + +interface Glyph extends React.FunctionComponent { + layOut?: Function +} + +export function chooseGlyphComponent(feature: Feature): Glyph { + const type = feature.get('type') + const strand = feature.get('strand') + const subfeatures: Feature[] = feature.get('subfeatures') + + if (subfeatures) { + const hasSubSub = subfeatures.find(subfeature => { + return !!subfeature.get('subfeatures') + }) + if (hasSubSub) { + return Subfeatures + } + const transcriptTypes = ['mRNA', 'transcript'] + if ( + transcriptTypes.includes(type) && + subfeatures.find(f => f.get('type') === 'CDS') + ) { + return ProcessedTranscript + } + return Segments + } + return [1, -1].includes(strand) ? Chevron : Box +} + +interface BaseLayOutArgs { + layout: SceneGraph + bpPerPx: number + reversed: boolean + config: AnyConfigurationModel +} + +interface FeatureLayOutArgs extends BaseLayOutArgs { + feature: Feature +} + +interface SubfeatureLayOutArgs extends BaseLayOutArgs { + subfeatures: Feature[] +} + +export function layOut({ + layout, + feature, + bpPerPx, + reversed, + config, +}: FeatureLayOutArgs): SceneGraph { + const displayMode = readConfObject(config, 'displayMode') + const subLayout = layOutFeature({ + layout, + feature, + bpPerPx, + reversed, + config, + }) + if (displayMode !== 'reducedRepresentation') { + layOutSubfeatures({ + layout: subLayout, + subfeatures: feature.get('subfeatures') || [], + bpPerPx, + reversed, + config, + }) + } + return subLayout +} + +export function layOutFeature(args: FeatureLayOutArgs): SceneGraph { + const { layout, feature, bpPerPx, reversed, config } = args + const displayMode = readConfObject(config, 'displayMode') + const GlyphComponent = + displayMode === 'reducedRepresentation' + ? Chevron + : chooseGlyphComponent(feature) + const parentFeature = feature.parent() + let x = 0 + if (parentFeature) { + x = reversed + ? (parentFeature.get('end') - feature.get('end')) / bpPerPx + : (feature.get('start') - parentFeature.get('start')) / bpPerPx + } + const height = readConfObject(config, 'height', { feature }) + const width = (feature.get('end') - feature.get('start')) / bpPerPx + const layoutParent = layout.parent + const top = layoutParent ? layoutParent.top : 0 + const subLayout = layout.addChild( + String(feature.id()), + x, + displayMode === 'collapse' ? 0 : top, + width, + displayMode === 'compact' ? height / 2 : height, + { GlyphComponent }, + ) + return subLayout +} + +export function layOutSubfeatures(args: SubfeatureLayOutArgs): void { + const { layout: subLayout, subfeatures, bpPerPx, reversed, config } = args + subfeatures.forEach(subfeature => { + const SubfeatureGlyphComponent = chooseGlyphComponent(subfeature) + ;(SubfeatureGlyphComponent.layOut || layOut)({ + layout: subLayout, + feature: subfeature, + bpPerPx, + reversed, + config, + }) + }) +} + +export function isUTR(feature: Feature): boolean { + return /(\bUTR|_UTR|untranslated[_\s]region)\b/.test( + feature.get('type') || '', + ) +} diff --git a/plugins/canvas/src/CanvasFeatureRenderer/configSchema.ts b/plugins/canvas/src/CanvasFeatureRenderer/configSchema.ts new file mode 100644 index 0000000000..b4fdc3ad31 --- /dev/null +++ b/plugins/canvas/src/CanvasFeatureRenderer/configSchema.ts @@ -0,0 +1,109 @@ +import { ConfigurationSchema } from '@jbrowse/core/configuration' +import { types } from 'mobx-state-tree' + +export default ConfigurationSchema( + 'SvgFeatureRenderer', + { + color1: { + type: 'color', + description: 'the main color of each feature', + defaultValue: 'goldenrod', + contextVariable: ['feature'], + }, + color2: { + type: 'color', + description: + 'the secondary color of each feature, used for connecting lines, etc', + defaultValue: 'black', + contextVariable: ['feature'], + }, + color3: { + type: 'color', + description: + 'the tertiary color of each feature, often used for contrasting fills, like on UTRs', + defaultValue: '#357089', + contextVariable: ['feature'], + }, + height: { + type: 'number', + description: 'height in pixels of the main body of each feature', + defaultValue: 10, + contextVariable: ['feature'], + }, + showLabels: { + type: 'boolean', + defaultValue: true, + }, + showDescriptions: { + type: 'boolean', + defaultValue: true, + }, + labels: ConfigurationSchema('SvgFeatureLabels', { + name: { + type: 'string', + description: + 'the primary name of the feature to show, if space is available', + defaultValue: `jexl:get(feature,'name') || get(feature,'id')`, + contextVariable: ['feature'], + }, + nameColor: { + type: 'color', + description: 'the color of the name label, if shown', + defaultValue: 'black', + contextVariable: ['feature'], + }, + description: { + type: 'string', + description: 'the text description to show, if space is available', + defaultValue: `jexl:get(feature,'note') || get(feature,'description')`, + contextVariable: ['feature'], + }, + descriptionColor: { + type: 'color', + description: 'the color of the description, if shown', + defaultValue: 'blue', + contextVariable: ['feature'], + }, + fontSize: { + type: 'number', + description: + 'height in pixels of the text to use for names and descriptions', + defaultValue: 13, + contextVariable: ['feature'], + }, + }), + displayMode: { + type: 'stringEnum', + model: types.enumeration('displayMode', [ + 'normal', + 'compact', + 'reducedRepresentation', + 'collapse', + ]), + description: 'Alternative display modes', + defaultValue: 'normal', + }, + maxFeatureGlyphExpansion: { + type: 'number', + description: + "maximum number of pixels on each side of a feature's bounding coordinates that a glyph is allowed to use", + defaultValue: 500, + }, + maxHeight: { + type: 'integer', + description: 'the maximum height to be used in a svg rendering', + defaultValue: 600, + }, + subParts: { + type: 'string', + description: 'subparts for a glyph', + defaultValue: 'CDS,UTR,five_prime_UTR,three_prime_UTR', + }, + impliedUTRs: { + type: 'boolean', + description: 'imply UTR from the exon and CDS differences', + defaultValue: false, + }, + }, + { explicitlyTyped: true }, +) diff --git a/plugins/canvas/src/CanvasFeatureRenderer/index.ts b/plugins/canvas/src/CanvasFeatureRenderer/index.ts new file mode 100644 index 0000000000..0f8629b999 --- /dev/null +++ b/plugins/canvas/src/CanvasFeatureRenderer/index.ts @@ -0,0 +1,2 @@ +export { default as ReactComponent } from './components/SvgFeatureRendering' +export { default as configSchema } from './configSchema' diff --git a/plugins/canvas/src/index.ts b/plugins/canvas/src/index.ts new file mode 100644 index 0000000000..be6142d1a0 --- /dev/null +++ b/plugins/canvas/src/index.ts @@ -0,0 +1,32 @@ +import BoxRendererType from '@jbrowse/core/pluggableElementTypes/renderers/BoxRendererType' +import Plugin from '@jbrowse/core/Plugin' +import PluginManager from '@jbrowse/core/PluginManager' +import { + configSchema as canvasFeatureRendererConfigSchema, + ReactComponent as CanvasFeatureRendererReactComponent, +} from './CanvasFeatureRenderer' + +class CanvasFeatureRenderer extends BoxRendererType { + supportsSVG = true +} + +export default class CanvasPlugin extends Plugin { + name = 'CanvasPlugin' + + install(pluginManager: PluginManager) { + pluginManager.addRendererType( + () => + new CanvasFeatureRenderer({ + name: 'CanvasFeatureRenderer', + ReactComponent: CanvasFeatureRendererReactComponent, + configSchema: canvasFeatureRendererConfigSchema, + pluginManager, + }), + ) + } +} + +export { + canvasFeatureRendererConfigSchema, + CanvasFeatureRendererReactComponent, +} diff --git a/plugins/canvas/tsconfig.json b/plugins/canvas/tsconfig.json new file mode 100644 index 0000000000..ca7cf4fc83 --- /dev/null +++ b/plugins/canvas/tsconfig.json @@ -0,0 +1,10 @@ +{ + "extends": "../../tsconfig.json", + "include": ["src", "types"], + "compilerOptions": { + "importHelpers": true, + "declaration": true, + "sourceMap": true, + "rootDir": "./src" + } +} diff --git a/plugins/canvas/tsdx.config.js b/plugins/canvas/tsdx.config.js new file mode 100644 index 0000000000..02db3c28c4 --- /dev/null +++ b/plugins/canvas/tsdx.config.js @@ -0,0 +1,8 @@ +// Not transpiled with TypeScript or Babel, so use plain Es6/Node.js! +module.exports = { + // This function will run for each entry/format/env combination + rollup(config) { + config.inlineDynamicImports = true + return config // always return a config. + }, +} From 2d66055f5aaec4c1988f27cf021fa9c80d1e005c Mon Sep 17 00:00:00 2001 From: Colin Date: Sat, 23 Oct 2021 15:03:51 -0400 Subject: [PATCH 02/28] Fix issue with server-side warning in tests --- .../components/PileupRendering.tsx | 8 +- .../components/SvgFeatureRendering.js | 180 ++++++++++++++-- .../components/SvgOverlay.tsx | 204 +----------------- 3 files changed, 166 insertions(+), 226 deletions(-) diff --git a/plugins/alignments/src/PileupRenderer/components/PileupRendering.tsx b/plugins/alignments/src/PileupRenderer/components/PileupRendering.tsx index 7d39cafa10..746dc8b22d 100644 --- a/plugins/alignments/src/PileupRenderer/components/PileupRendering.tsx +++ b/plugins/alignments/src/PileupRenderer/components/PileupRendering.tsx @@ -136,9 +136,11 @@ function PileupRendering(props: { } let offsetX = 0 let offsetY = 0 - if (highlightOverlayCanvas.current) { - offsetX = highlightOverlayCanvas.current.getBoundingClientRect().left - offsetY = highlightOverlayCanvas.current.getBoundingClientRect().top + const canvas = highlightOverlayCanvas.current + if (canvas) { + const { left, top } = canvas.getBoundingClientRect() + offsetX = left + offsetY = top } offsetX = event.clientX - offsetX offsetY = event.clientY - offsetY diff --git a/plugins/svg/src/SvgFeatureRenderer/components/SvgFeatureRendering.js b/plugins/svg/src/SvgFeatureRenderer/components/SvgFeatureRendering.js index 25c50e70e4..6478f68ee9 100644 --- a/plugins/svg/src/SvgFeatureRenderer/components/SvgFeatureRendering.js +++ b/plugins/svg/src/SvgFeatureRenderer/components/SvgFeatureRendering.js @@ -6,13 +6,9 @@ import { observer } from 'mobx-react' import ReactPropTypes from 'prop-types' import React, { useEffect, useRef, useState, useCallback } from 'react' import FeatureGlyph from './FeatureGlyph' -import SvgOverlay from './SvgOverlay' +import { OverlayRect } from './SvgOverlay' import { chooseGlyphComponent, layOut } from './util' -const renderingStyle = { - position: 'relative', -} - // used to make features have a little padding for their labels const nameWidthPadding = 2 const textVerticalPadding = 2 @@ -175,26 +171,30 @@ function SvgFeatureRendering(props) { config, displayModel, exportSVG, + onMouseOut, + onMouseDown, + onMouseLeave, + onMouseEnter, + onMouseOver, + onMouseMove, + onMouseUp, + onClick, + ...handlers } = props + const { selectedFeatureId, featureIdUnderMouse, contextMenuFeature } = + displayModel const [region] = regions || [] const width = (region.end - region.start) / bpPerPx const displayMode = readConfObject(config, 'displayMode') + const [highlightRect, setHighlightRect] = useState() + const [mouseoverRect, setMouseoverRect] = useState() const ref = useRef() const [mouseIsDown, setMouseIsDown] = useState(false) const [movedDuringLastMouseDown, setMovedDuringLastMouseDown] = useState(false) const [height, setHeight] = useState(0) - const { - onMouseOut, - onMouseDown, - onMouseLeave, - onMouseEnter, - onMouseOver, - onMouseMove, - onMouseUp, - onClick, - } = props + const mouseoverFeatureId = featureIdUnderMouse || contextMenuFeature?.id() const mouseDown = useCallback( event => { @@ -272,9 +272,12 @@ function SvgFeatureRendering(props) { } let offsetX = 0 let offsetY = 0 - if (ref.current) { - offsetX = ref.current.getBoundingClientRect().left - offsetY = ref.current.getBoundingClientRect().top + + const canvas = ref.current + if (canvas) { + const { left, top } = canvas.getBoundingClientRect() + offsetX = left + offsetY = top } offsetX = event.clientX - offsetX offsetY = event.clientY - offsetY @@ -316,10 +319,117 @@ function SvgFeatureRendering(props) { [movedDuringLastMouseDown, onClick], ) + function onFeatureMouseDown( + event: React.MouseEvent, + ) { + const { onFeatureMouseDown: handler } = handlers + if (!(handler && mouseoverFeatureId)) { + return undefined + } + return handler(event, mouseoverFeatureId) + } + + function onFeatureMouseEnter( + event: React.MouseEvent, + ) { + const { onFeatureMouseEnter: handler } = handlers + if (!(handler && mouseoverFeatureId)) { + return undefined + } + return handler(event, mouseoverFeatureId) + } + + function onFeatureMouseOut( + event: + | React.MouseEvent + | React.FocusEvent, + ) { + const { onFeatureMouseOut: handler } = handlers + if (!(handler && mouseoverFeatureId)) { + return undefined + } + return handler(event, mouseoverFeatureId) + } + + function onFeatureMouseOver( + event: + | React.MouseEvent + | React.FocusEvent, + ) { + const { onFeatureMouseOver: handler } = handlers + if (!(handler && mouseoverFeatureId)) { + return undefined + } + return handler(event, mouseoverFeatureId) + } + + function onFeatureMouseUp( + event: React.MouseEvent, + ) { + const { onFeatureMouseUp: handler } = handlers + if (!(handler && mouseoverFeatureId)) { + return undefined + } + return handler(event, mouseoverFeatureId) + } + + function onFeatureMouseLeave( + event: React.MouseEvent, + ) { + const { onFeatureMouseLeave: handler } = handlers + if (!(handler && mouseoverFeatureId)) { + return undefined + } + return handler(event, mouseoverFeatureId) + } + + function onFeatureMouseMove( + event: React.MouseEvent, + ) { + const { onFeatureMouseMove: handler } = handlers + if (!(handler && mouseoverFeatureId)) { + return undefined + } + return handler(event, mouseoverFeatureId) + } + + function onFeatureClick(event: React.MouseEvent) { + if (movedDuringLastMouseDown) { + return undefined + } + const { onFeatureClick: handler } = handlers + if (!(handler && mouseoverFeatureId)) { + return undefined + } + event.stopPropagation() + return handler(event, mouseoverFeatureId) + } + + function onFeatureContextMenu( + event: React.MouseEvent, + ) { + const { onFeatureContextMenu: handler } = handlers + if (!(handler && mouseoverFeatureId)) { + return undefined + } + return handler(event, mouseoverFeatureId) + } + useEffect(() => { setHeight(layout.getTotalHeight()) }, [layout]) + useEffect(() => { + setMouseoverRect( + displayModel.getFeatureByID?.(blockKey, featureIdUnderMouse), + ) + }, [blockKey, displayModel, featureIdUnderMouse]) + + useEffect(() => { + setHighlightRect(displayModel.getFeatureByID?.(blockKey, selectedFeatureId)) + }, [blockKey, displayModel, selectedFeatureId]) + + if (exportSVG) { return ( +
- + {mouseoverRect ? ( + + ) : null} + {highlightRect ? ( + + ) : null}
) @@ -379,6 +518,7 @@ SvgFeatureRendering.propTypes = { configuration: ReactPropTypes.shape({}), getFeatureOverlapping: ReactPropTypes.func, selectedFeatureId: ReactPropTypes.string, + contextMenuFeature: ReactPropTypes.shape({ id: ReactPropTypes.func }), featureIdUnderMouse: ReactPropTypes.string, }), diff --git a/plugins/svg/src/SvgFeatureRenderer/components/SvgOverlay.tsx b/plugins/svg/src/SvgFeatureRenderer/components/SvgOverlay.tsx index 469d2f7ecf..fbbc616b47 100644 --- a/plugins/svg/src/SvgFeatureRenderer/components/SvgOverlay.tsx +++ b/plugins/svg/src/SvgFeatureRenderer/components/SvgOverlay.tsx @@ -1,63 +1,8 @@ import { bpSpanPx } from '@jbrowse/core/util' -import SimpleFeature from '@jbrowse/core/util/simpleFeature' import { Region } from '@jbrowse/core/util/types' -import { observer } from 'mobx-react' import React from 'react' type LayoutRecord = [number, number, number, number] -interface SvgOverlayProps { - region: Region - displayModel: { - getFeatureByID: (arg0: string, arg1: string) => LayoutRecord - selectedFeatureId?: string - featureIdUnderMouse?: string - contextMenuFeature?: SimpleFeature - } - bpPerPx: number - blockKey: string - movedDuringLastMouseDown: boolean - onFeatureMouseDown?( - event: React.MouseEvent, - featureId: string, - ): {} - onFeatureMouseEnter?( - event: React.MouseEvent, - featureId: string, - ): {} - onFeatureMouseOut?( - event: - | React.MouseEvent - | React.FocusEvent, - featureId: string, - ): {} - onFeatureMouseOver?( - event: - | React.MouseEvent - | React.FocusEvent, - featureId: string, - ): {} - onFeatureMouseUp?( - event: React.MouseEvent, - featureId: string, - ): {} - onFeatureMouseLeave?( - event: React.MouseEvent, - featureId: string, - ): {} - onFeatureMouseMove?( - event: React.MouseEvent, - featureId: string, - ): {} - // synthesized from mouseup and mousedown - onFeatureClick?( - event: React.MouseEvent, - featureId: string, - ): {} - onFeatureContextMenu?( - event: React.MouseEvent, - featureId: string, - ): {} -} interface OverlayRectProps extends React.SVGProps { rect?: LayoutRecord @@ -65,7 +10,7 @@ interface OverlayRectProps extends React.SVGProps { bpPerPx: number } -function OverlayRect({ +export function OverlayRect({ rect, region, bpPerPx, @@ -98,150 +43,3 @@ function OverlayRect({ /> ) } - -function SvgOverlay({ - displayModel, - blockKey, - region, - bpPerPx, - movedDuringLastMouseDown, - ...handlers -}: SvgOverlayProps) { - const { selectedFeatureId, featureIdUnderMouse, contextMenuFeature } = - displayModel - - const mouseoverFeatureId = featureIdUnderMouse || contextMenuFeature?.id() - - function onFeatureMouseDown( - event: React.MouseEvent, - ) { - const { onFeatureMouseDown: handler } = handlers - if (!(handler && mouseoverFeatureId)) { - return undefined - } - return handler(event, mouseoverFeatureId) - } - - function onFeatureMouseEnter( - event: React.MouseEvent, - ) { - const { onFeatureMouseEnter: handler } = handlers - if (!(handler && mouseoverFeatureId)) { - return undefined - } - return handler(event, mouseoverFeatureId) - } - - function onFeatureMouseOut( - event: - | React.MouseEvent - | React.FocusEvent, - ) { - const { onFeatureMouseOut: handler } = handlers - if (!(handler && mouseoverFeatureId)) { - return undefined - } - return handler(event, mouseoverFeatureId) - } - - function onFeatureMouseOver( - event: - | React.MouseEvent - | React.FocusEvent, - ) { - const { onFeatureMouseOver: handler } = handlers - if (!(handler && mouseoverFeatureId)) { - return undefined - } - return handler(event, mouseoverFeatureId) - } - - function onFeatureMouseUp( - event: React.MouseEvent, - ) { - const { onFeatureMouseUp: handler } = handlers - if (!(handler && mouseoverFeatureId)) { - return undefined - } - return handler(event, mouseoverFeatureId) - } - - function onFeatureMouseLeave( - event: React.MouseEvent, - ) { - const { onFeatureMouseLeave: handler } = handlers - if (!(handler && mouseoverFeatureId)) { - return undefined - } - return handler(event, mouseoverFeatureId) - } - - function onFeatureMouseMove( - event: React.MouseEvent, - ) { - const { onFeatureMouseMove: handler } = handlers - if (!(handler && mouseoverFeatureId)) { - return undefined - } - return handler(event, mouseoverFeatureId) - } - - function onFeatureClick(event: React.MouseEvent) { - if (movedDuringLastMouseDown) { - return undefined - } - const { onFeatureClick: handler } = handlers - if (!(handler && mouseoverFeatureId)) { - return undefined - } - event.stopPropagation() - return handler(event, mouseoverFeatureId) - } - - function onFeatureContextMenu( - event: React.MouseEvent, - ) { - const { onFeatureContextMenu: handler } = handlers - if (!(handler && mouseoverFeatureId)) { - return undefined - } - return handler(event, mouseoverFeatureId) - } - - return ( - <> - {mouseoverFeatureId ? ( - - ) : null} - {selectedFeatureId ? ( - - ) : null} - - ) -} - -export default observer(SvgOverlay) From 9064ed3a273fa4bf507f4d7409db249543634197 Mon Sep 17 00:00:00 2001 From: Colin Date: Sat, 23 Oct 2021 15:04:16 -0400 Subject: [PATCH 03/28] More canvas --- products/jbrowse-web/package.json | 1 + products/jbrowse-web/src/corePlugins.ts | 2 ++ 2 files changed, 3 insertions(+) diff --git a/products/jbrowse-web/package.json b/products/jbrowse-web/package.json index d55bacac80..2a522b7027 100644 --- a/products/jbrowse-web/package.json +++ b/products/jbrowse-web/package.json @@ -38,6 +38,7 @@ "@jbrowse/plugin-spreadsheet-view": "^1.5.0", "@jbrowse/plugin-sv-inspector": "^1.5.0", "@jbrowse/plugin-svg": "^1.5.0", + "@jbrowse/plugin-canvas": "^1.5.0", "@jbrowse/plugin-trackhub-registry": "^1.5.0", "@jbrowse/plugin-trix": "^1.5.0", "@jbrowse/plugin-variants": "^1.5.0", diff --git a/products/jbrowse-web/src/corePlugins.ts b/products/jbrowse-web/src/corePlugins.ts index d53c301711..81bdf25269 100644 --- a/products/jbrowse-web/src/corePlugins.ts +++ b/products/jbrowse-web/src/corePlugins.ts @@ -15,6 +15,7 @@ import Menus from '@jbrowse/plugin-menus' import RDF from '@jbrowse/plugin-rdf' import Sequence from '@jbrowse/plugin-sequence' import SVG from '@jbrowse/plugin-svg' +import Canvas from '@jbrowse/plugin-canvas' import TrackHubRegistry from '@jbrowse/plugin-trackhub-registry' import Variants from '@jbrowse/plugin-variants' import Wiggle from '@jbrowse/plugin-wiggle' @@ -26,6 +27,7 @@ import GridBookmarkPlugin from '@jbrowse/plugin-grid-bookmark' const corePlugins = [ SVG, + Canvas, LinearGenomeView, Alignments, Authentication, From fe856979ddcd71a7f712fd75f1154d11d4bc318a Mon Sep 17 00:00:00 2001 From: Colin Date: Sat, 23 Oct 2021 16:05:53 -0400 Subject: [PATCH 04/28] Test --- .../components/SvgFeatureRendering.js | 250 +----------------- .../src/LinearBasicDisplay/model.ts | 1 + products/jbrowse-web/src/corePlugins.ts | 2 +- 3 files changed, 3 insertions(+), 250 deletions(-) diff --git a/plugins/canvas/src/CanvasFeatureRenderer/components/SvgFeatureRendering.js b/plugins/canvas/src/CanvasFeatureRenderer/components/SvgFeatureRendering.js index 25c50e70e4..5a3e6ff8de 100644 --- a/plugins/canvas/src/CanvasFeatureRenderer/components/SvgFeatureRendering.js +++ b/plugins/canvas/src/CanvasFeatureRenderer/components/SvgFeatureRendering.js @@ -166,255 +166,7 @@ RenderedFeatures.defaultProps = { } function SvgFeatureRendering(props) { - const { - layout, - blockKey, - regions, - bpPerPx, - features, - config, - displayModel, - exportSVG, - } = props - const [region] = regions || [] - const width = (region.end - region.start) / bpPerPx - const displayMode = readConfObject(config, 'displayMode') - - const ref = useRef() - const [mouseIsDown, setMouseIsDown] = useState(false) - const [movedDuringLastMouseDown, setMovedDuringLastMouseDown] = - useState(false) - const [height, setHeight] = useState(0) - const { - onMouseOut, - onMouseDown, - onMouseLeave, - onMouseEnter, - onMouseOver, - onMouseMove, - onMouseUp, - onClick, - } = props - - const mouseDown = useCallback( - event => { - setMouseIsDown(true) - setMovedDuringLastMouseDown(false) - const handler = onMouseDown - if (!handler) { - return undefined - } - return handler(event) - }, - [onMouseDown], - ) - - const mouseUp = useCallback( - event => { - setMouseIsDown(false) - const handler = onMouseUp - if (!handler) { - return undefined - } - return handler(event) - }, - [onMouseUp], - ) - - const mouseEnter = useCallback( - event => { - const handler = onMouseEnter - if (!handler) { - return undefined - } - return handler(event) - }, - [onMouseEnter], - ) - - const mouseLeave = useCallback( - event => { - const handler = onMouseLeave - if (!handler) { - return undefined - } - return handler(event) - }, - [onMouseLeave], - ) - - const mouseOver = useCallback( - event => { - const handler = onMouseOver - if (!handler) { - return undefined - } - return handler(event) - }, - [onMouseOver], - ) - - const mouseOut = useCallback( - event => { - const handler = onMouseOut - if (!handler) { - return undefined - } - return handler(event) - }, - [onMouseOut], - ) - - const mouseMove = useCallback( - event => { - if (mouseIsDown) { - setMovedDuringLastMouseDown(true) - } - let offsetX = 0 - let offsetY = 0 - if (ref.current) { - offsetX = ref.current.getBoundingClientRect().left - offsetY = ref.current.getBoundingClientRect().top - } - offsetX = event.clientX - offsetX - offsetY = event.clientY - offsetY - const px = region.reversed ? width - offsetX : offsetX - const clientBp = region.start + bpPerPx * px - - const featureIdCurrentlyUnderMouse = displayModel.getFeatureOverlapping( - blockKey, - clientBp, - offsetY, - ) - - if (onMouseMove) { - onMouseMove(event, featureIdCurrentlyUnderMouse) - } - }, - [ - blockKey, - bpPerPx, - mouseIsDown, - onMouseMove, - region.reversed, - region.start, - displayModel, - width, - ], - ) - - const click = useCallback( - event => { - // don't select a feature if we are clicking and dragging - if (movedDuringLastMouseDown) { - return - } - if (onClick) { - onClick(event) - } - }, - [movedDuringLastMouseDown, onClick], - ) - - useEffect(() => { - setHeight(layout.getTotalHeight()) - }, [layout]) - - if (exportSVG) { - return ( - - ) - } - return ( -
- - - - -
- ) -} - -SvgFeatureRendering.propTypes = { - layout: ReactPropTypes.shape({ - addRect: ReactPropTypes.func.isRequired, - getTotalHeight: ReactPropTypes.func.isRequired, - }).isRequired, - - regions: ReactPropTypes.arrayOf(CommonPropTypes.Region).isRequired, - bpPerPx: ReactPropTypes.number.isRequired, - features: ReactPropTypes.oneOfType([ - ReactPropTypes.instanceOf(Map), - ReactPropTypes.arrayOf(ReactPropTypes.shape()), - ]), - config: CommonPropTypes.ConfigSchema.isRequired, - displayModel: ReactPropTypes.shape({ - configuration: ReactPropTypes.shape({}), - getFeatureOverlapping: ReactPropTypes.func, - selectedFeatureId: ReactPropTypes.string, - featureIdUnderMouse: ReactPropTypes.string, - }), - - onMouseDown: ReactPropTypes.func, - onMouseUp: ReactPropTypes.func, - onMouseEnter: ReactPropTypes.func, - onMouseLeave: ReactPropTypes.func, - onMouseOver: ReactPropTypes.func, - onMouseOut: ReactPropTypes.func, - onMouseMove: ReactPropTypes.func, - onClick: ReactPropTypes.func, - onContextMenu: ReactPropTypes.func, - onFeatureClick: ReactPropTypes.func, - onFeatureContextMenu: ReactPropTypes.func, - blockKey: ReactPropTypes.string, - exportSVG: ReactPropTypes.shape({}), -} - -SvgFeatureRendering.defaultProps = { - displayModel: {}, - exportSVG: undefined, - - features: new Map(), - blockKey: undefined, - - onMouseDown: undefined, - onMouseUp: undefined, - onMouseEnter: undefined, - onMouseLeave: undefined, - onMouseOver: undefined, - onMouseOut: undefined, - onMouseMove: undefined, - onClick: undefined, - onContextMenu: undefined, - onFeatureClick: undefined, - onFeatureContextMenu: undefined, + return 'hello' } export default observer(SvgFeatureRendering) diff --git a/plugins/linear-genome-view/src/LinearBasicDisplay/model.ts b/plugins/linear-genome-view/src/LinearBasicDisplay/model.ts index 30cbd4aacb..6fb470d792 100644 --- a/plugins/linear-genome-view/src/LinearBasicDisplay/model.ts +++ b/plugins/linear-genome-view/src/LinearBasicDisplay/model.ts @@ -93,6 +93,7 @@ const stateModelFactory = (configSchema: AnyConfigurationSchemaType) => return { renderProps() { const config = self.rendererConfig + console.log(config) return { ...superRenderProps(), diff --git a/products/jbrowse-web/src/corePlugins.ts b/products/jbrowse-web/src/corePlugins.ts index 81bdf25269..d94d719cb8 100644 --- a/products/jbrowse-web/src/corePlugins.ts +++ b/products/jbrowse-web/src/corePlugins.ts @@ -26,8 +26,8 @@ import TrixPlugin from '@jbrowse/plugin-trix' import GridBookmarkPlugin from '@jbrowse/plugin-grid-bookmark' const corePlugins = [ - SVG, Canvas, + SVG, LinearGenomeView, Alignments, Authentication, From f136de1bfe13e3e53e8af5d71184761646f4eeef Mon Sep 17 00:00:00 2001 From: Colin Date: Sat, 23 Oct 2021 16:11:44 -0400 Subject: [PATCH 05/28] Remove some svg stuff --- .../src/CanvasFeatureRenderer/README.md | 33 - .../CanvasFeatureRenderer/components/Box.js | 66 -- .../components/CanvasFeatureRendering.js | 8 + .../components/Chevron.js | 113 -- .../components/FeatureGlyph.js | 111 -- .../components/FeatureLabel.tsx | 46 - .../components/ProcessedTranscript.js | 175 --- .../components/Segments.js | 104 -- .../components/Subfeatures.js | 81 -- .../components/SvgFeatureRendering.js | 172 --- .../components/SvgFeatureRendering.test.js | 1041 ----------------- .../components/SvgOverlay.tsx | 247 ---- .../SvgFeatureRendering.test.js.snap | 585 --------- .../CanvasFeatureRenderer/components/util.ts | 129 -- .../src/CanvasFeatureRenderer/configSchema.ts | 6 +- .../canvas/src/CanvasFeatureRenderer/index.ts | 2 +- .../src/LinearBasicDisplay/model.ts | 43 +- 17 files changed, 34 insertions(+), 2928 deletions(-) delete mode 100644 plugins/canvas/src/CanvasFeatureRenderer/README.md delete mode 100644 plugins/canvas/src/CanvasFeatureRenderer/components/Box.js create mode 100644 plugins/canvas/src/CanvasFeatureRenderer/components/CanvasFeatureRendering.js delete mode 100644 plugins/canvas/src/CanvasFeatureRenderer/components/Chevron.js delete mode 100644 plugins/canvas/src/CanvasFeatureRenderer/components/FeatureGlyph.js delete mode 100644 plugins/canvas/src/CanvasFeatureRenderer/components/FeatureLabel.tsx delete mode 100644 plugins/canvas/src/CanvasFeatureRenderer/components/ProcessedTranscript.js delete mode 100644 plugins/canvas/src/CanvasFeatureRenderer/components/Segments.js delete mode 100644 plugins/canvas/src/CanvasFeatureRenderer/components/Subfeatures.js delete mode 100644 plugins/canvas/src/CanvasFeatureRenderer/components/SvgFeatureRendering.js delete mode 100644 plugins/canvas/src/CanvasFeatureRenderer/components/SvgFeatureRendering.test.js delete mode 100644 plugins/canvas/src/CanvasFeatureRenderer/components/SvgOverlay.tsx delete mode 100644 plugins/canvas/src/CanvasFeatureRenderer/components/__snapshots__/SvgFeatureRendering.test.js.snap delete mode 100644 plugins/canvas/src/CanvasFeatureRenderer/components/util.ts diff --git a/plugins/canvas/src/CanvasFeatureRenderer/README.md b/plugins/canvas/src/CanvasFeatureRenderer/README.md deleted file mode 100644 index 0a4e19213c..0000000000 --- a/plugins/canvas/src/CanvasFeatureRenderer/README.md +++ /dev/null @@ -1,33 +0,0 @@ -# SVG Feature Renderer - -## Process - -The SVG Feature renderer has two steps: - -- **Layout**: Each feature, subfeature, and label is laid out so that they are - all allocated the proper space. First, `layOut` is called on the top-level - feature, and then that function is responsible for calling `layOut` on its - subfeatures. The default `layOut` function just calls `layOut` on all the - subfeatures. The `layOut` function can be customized by a glyph by providing a - `layOut` method on the Glyph function. This can be used to e.g. filter certain - subfeatures so they are not displayed or manipulate the position of the - subfeatures. - -- **Render**: Each glyph is a React component that returns a valid SVG element. - It positions itself using the `featureLayout` prop. Each rendered feature has its own glyph. - -## Glyphs - -- **Box**: A simple rectangle from the start to the end of a feature. - -- **Chevron**: A rectangle that is pointed/indented at the ends. It points - itself according to the feature's strand. - -- **ProcessedTranscript**: A `Segments` glyph that only lays out/renders its - subfeatures that are of type CDS, UTR, five_prime_UTR, or three_prime_UTR. - -- **Segments**: A glyph that draws a line from the start to the end of a - feature. The subfeatures are drawn on top of this line. - -- **Subfeatures**: Does not render the feature itself, but renders its - subfeatures so they are vertically offset from each other. diff --git a/plugins/canvas/src/CanvasFeatureRenderer/components/Box.js b/plugins/canvas/src/CanvasFeatureRenderer/components/Box.js deleted file mode 100644 index beacfcee48..0000000000 --- a/plugins/canvas/src/CanvasFeatureRenderer/components/Box.js +++ /dev/null @@ -1,66 +0,0 @@ -import { readConfObject } from '@jbrowse/core/configuration' -import { PropTypes as CommonPropTypes } from '@jbrowse/core/util/types/mst' -import { emphasize } from '@jbrowse/core/util/color' -import { observer } from 'mobx-react' -import ReactPropTypes from 'prop-types' -import React from 'react' - -function Box(props) { - const { feature, region, config, featureLayout, selected, bpPerPx } = props - const screenWidth = (region.end - region.start) / bpPerPx - - const color1 = readConfObject(config, 'color1', { feature }) - let emphasizedColor1 - try { - emphasizedColor1 = emphasize(color1, 0.3) - } catch (error) { - emphasizedColor1 = color1 - } - const color2 = readConfObject(config, 'color2', { feature }) - - const { left, top, width, height } = featureLayout.absolute - - if (left + width < 0) { - return null - } - const leftWithinBlock = Math.max(left, 0) - const diff = leftWithinBlock - left - const widthWithinBlock = Math.max(1, Math.min(width - diff, screenWidth)) - - return ( - - ) -} - -Box.propTypes = { - feature: ReactPropTypes.shape({ - get: ReactPropTypes.func.isRequired, - id: ReactPropTypes.func.isRequired, - }).isRequired, - region: CommonPropTypes.Region.isRequired, - bpPerPx: ReactPropTypes.number.isRequired, - featureLayout: ReactPropTypes.shape({ - absolute: ReactPropTypes.shape({ - top: ReactPropTypes.number.isRequired, - left: ReactPropTypes.number.isRequired, - width: ReactPropTypes.number.isRequired, - height: ReactPropTypes.number.isRequired, - }), - }).isRequired, - selected: ReactPropTypes.bool, - config: CommonPropTypes.ConfigSchema.isRequired, -} - -Box.defaultProps = { - selected: false, -} - -export default observer(Box) diff --git a/plugins/canvas/src/CanvasFeatureRenderer/components/CanvasFeatureRendering.js b/plugins/canvas/src/CanvasFeatureRenderer/components/CanvasFeatureRendering.js new file mode 100644 index 0000000000..4faa321704 --- /dev/null +++ b/plugins/canvas/src/CanvasFeatureRenderer/components/CanvasFeatureRendering.js @@ -0,0 +1,8 @@ +import React from 'react' +import { observer } from 'mobx-react' + +function CanvasFeatureRendering(props) { + return 'hello' +} + +export default observer(CanvasFeatureRendering) diff --git a/plugins/canvas/src/CanvasFeatureRenderer/components/Chevron.js b/plugins/canvas/src/CanvasFeatureRenderer/components/Chevron.js deleted file mode 100644 index 59b9f06f3f..0000000000 --- a/plugins/canvas/src/CanvasFeatureRenderer/components/Chevron.js +++ /dev/null @@ -1,113 +0,0 @@ -import { readConfObject } from '@jbrowse/core/configuration' -import { PropTypes as CommonPropTypes } from '@jbrowse/core/util/types/mst' -import { emphasize } from '@jbrowse/core/util/color' -import { observer } from 'mobx-react' -import ReactPropTypes from 'prop-types' -import React from 'react' -import { isUTR } from './util' - -const utrHeightFraction = 0.65 - -function Chevron(props) { - const { - feature, - bpPerPx, - region, - config, - featureLayout, - selected, - reversed, - } = props - - const screenWidth = (region.end - region.start) / bpPerPx - const width = Math.max(featureLayout.absolute.width, 1) - const { left } = featureLayout.absolute - let { top, height } = featureLayout.absolute - if (isUTR(feature)) { - top += ((1 - utrHeightFraction) / 2) * height - height *= utrHeightFraction - } - - const strand = feature.get('strand') - const direction = strand * (reversed ? -1 : 1) - const color = isUTR(feature) - ? readConfObject(config, 'color3', { feature }) - : readConfObject(config, 'color1', { feature }) - let emphasizedColor - try { - emphasizedColor = emphasize(color, 0.3) - } catch (error) { - emphasizedColor = color - } - const color2 = readConfObject(config, 'color2', { feature }) - - if (left + width < 0) { - return null - } - const leftWithinBlock = Math.max(left, 0) - const diff = leftWithinBlock - left - const widthWithinBlock = Math.max(1, Math.min(width - diff, screenWidth)) - - return ( - <> - - {direction < 0 && diff === 0 ? ( - - ) : null} - {direction > 0 && leftWithinBlock + widthWithinBlock < screenWidth ? ( - - ) : null} - - ) -} - -Chevron.propTypes = { - feature: ReactPropTypes.shape({ - id: ReactPropTypes.func.isRequired, - get: ReactPropTypes.func.isRequired, - }).isRequired, - region: CommonPropTypes.Region.isRequired, - bpPerPx: ReactPropTypes.number.isRequired, - featureLayout: ReactPropTypes.shape({ - absolute: ReactPropTypes.shape({ - top: ReactPropTypes.number.isRequired, - left: ReactPropTypes.number.isRequired, - width: ReactPropTypes.number.isRequired, - height: ReactPropTypes.number.isRequired, - }), - }).isRequired, - selected: ReactPropTypes.bool, - config: CommonPropTypes.ConfigSchema.isRequired, - reversed: ReactPropTypes.bool, -} - -Chevron.defaultProps = { - selected: false, - reversed: false, -} - -export default observer(Chevron) diff --git a/plugins/canvas/src/CanvasFeatureRenderer/components/FeatureGlyph.js b/plugins/canvas/src/CanvasFeatureRenderer/components/FeatureGlyph.js deleted file mode 100644 index e23bf6cd4c..0000000000 --- a/plugins/canvas/src/CanvasFeatureRenderer/components/FeatureGlyph.js +++ /dev/null @@ -1,111 +0,0 @@ -import { readConfObject } from '@jbrowse/core/configuration' -import { PropTypes as CommonPropTypes } from '@jbrowse/core/util/types/mst' -import { observer } from 'mobx-react' -import PropTypes from 'prop-types' -import React from 'react' -import FeatureLabel from './FeatureLabel' - -function FeatureGlyph(props) { - const { - feature, - rootLayout, - selected, - config, - name, - shouldShowName, - description, - shouldShowDescription, - fontHeight, - allowedWidthExpansion, - reversed, - } = props - - const featureLayout = rootLayout.getSubRecord(String(feature.id())) - const { GlyphComponent } = featureLayout.data - - const glyphComponents = [ - , - ] - - if (shouldShowName) { - glyphComponents.push( - , - ) - } - - if (shouldShowDescription) { - glyphComponents.push( - , - ) - } - - return {glyphComponents} -} - -FeatureGlyph.propTypes = { - feature: PropTypes.shape({ - id: PropTypes.func.isRequired, - get: PropTypes.func.isRequired, - }).isRequired, - layout: PropTypes.shape({ - addRect: PropTypes.func.isRequired, - getTotalHeight: PropTypes.func.isRequired, - }).isRequired, - rootLayout: PropTypes.shape({ - addChild: PropTypes.func.isRequired, - getSubRecord: PropTypes.func.isRequired, - }).isRequired, - region: CommonPropTypes.Region.isRequired, - bpPerPx: PropTypes.number.isRequired, - reversed: PropTypes.bool, - selected: PropTypes.bool, - config: CommonPropTypes.ConfigSchema.isRequired, - name: PropTypes.string, - shouldShowName: PropTypes.bool, - description: PropTypes.string, - shouldShowDescription: PropTypes.bool, - fontHeight: PropTypes.number, - allowedWidthExpansion: PropTypes.number, - movedDuringLastMouseDown: PropTypes.bool.isRequired, -} - -FeatureGlyph.defaultProps = { - reversed: false, - selected: false, - name: '', - shouldShowName: false, - description: '', - shouldShowDescription: false, - fontHeight: undefined, - allowedWidthExpansion: undefined, -} - -export default observer(FeatureGlyph) diff --git a/plugins/canvas/src/CanvasFeatureRenderer/components/FeatureLabel.tsx b/plugins/canvas/src/CanvasFeatureRenderer/components/FeatureLabel.tsx deleted file mode 100644 index f18fab8bf7..0000000000 --- a/plugins/canvas/src/CanvasFeatureRenderer/components/FeatureLabel.tsx +++ /dev/null @@ -1,46 +0,0 @@ -import React from 'react' -import { measureText } from '@jbrowse/core/util' - -export default function Label(props: { - text: string - x: number - y: number - color?: string - fontHeight?: number - featureWidth?: number - allowedWidthExpansion?: number - reversed?: boolean - fontWidthScaleFactor?: number -}) { - const { - text, - x, - y, - color = 'black', - fontHeight = 13, - featureWidth = 0, - reversed, - allowedWidthExpansion, - fontWidthScaleFactor = 0.6, - } = props - - const fontWidth = fontHeight * fontWidthScaleFactor - const totalWidth = - featureWidth && allowedWidthExpansion - ? featureWidth + allowedWidthExpansion - : Infinity - - const measuredTextWidth = measureText(text, fontHeight) - - return ( - - {measuredTextWidth > totalWidth - ? `${text.slice(0, totalWidth / fontWidth)}...` - : text} - - ) -} diff --git a/plugins/canvas/src/CanvasFeatureRenderer/components/ProcessedTranscript.js b/plugins/canvas/src/CanvasFeatureRenderer/components/ProcessedTranscript.js deleted file mode 100644 index 45d251bd47..0000000000 --- a/plugins/canvas/src/CanvasFeatureRenderer/components/ProcessedTranscript.js +++ /dev/null @@ -1,175 +0,0 @@ -import { observer } from 'mobx-react' -import React from 'react' -import SimpleFeature from '@jbrowse/core/util/simpleFeature' -import { readConfObject } from '@jbrowse/core/configuration' -import Segments from './Segments' -import { layOutFeature, layOutSubfeatures } from './util' - -function ProcessedTranscript(props) { - // eslint-disable-next-line react/prop-types - const { feature, config } = props - const subfeatures = getSubparts(feature, config) - - // we manually compute some subfeatures, so pass these separately - return -} - -// make a function that will filter features features according to the -// subParts conf var -function makeSubpartsFilter(confKey = 'subParts', config) { - let filter = readConfObject(config, confKey) - - if (typeof filter == 'string') { - // convert to array - filter = filter.split(/\s*,\s*/) - } - - if (Array.isArray(filter)) { - const typeNames = filter.map(typeName => typeName.toLowerCase()) - return feature => { - return typeNames.includes(feature.get('type').toLowerCase()) - } - } - if (typeof filter === 'function') { - return filter - } - return () => true -} - -function filterSubpart(feature, config) { - return makeSubpartsFilter('subParts', config)(feature) -} - -function isUTR(feature) { - return /(\bUTR|_UTR|untranslated[_\s]region)\b/.test( - feature.get('type') || '', - ) -} - -function makeUTRs(parent, subs) { - // based on Lincoln's UTR-making code in Bio::Graphics::Glyph::processed_transcript - const subparts = [...subs] - - let codeStart = Infinity - let codeEnd = -Infinity - - let haveLeftUTR - let haveRightUTR - - // gather exons, find coding start and end, and look for UTRs - const exons = [] - for (let i = 0; i < subparts.length; i++) { - const type = subparts[i].get('type') - if (/^cds/i.test(type)) { - if (codeStart > subparts[i].get('start')) { - codeStart = subparts[i].get('start') - } - if (codeEnd < subparts[i].get('end')) { - codeEnd = subparts[i].get('end') - } - } else if (/exon/i.test(type)) { - exons.push(subparts[i]) - } else if (isUTR(subparts[i])) { - haveLeftUTR = subparts[i].get('start') === parent.get('start') - haveRightUTR = subparts[i].get('end') === parent.get('end') - } - } - - // bail if we don't have exons and CDS - if (!(exons.length && codeStart < Infinity && codeEnd > -Infinity)) { - return subparts - } - - // make sure the exons are sorted by coord - exons.sort((a, b) => a.get('start') - b.get('start')) - - const strand = parent.get('strand') - - // make the left-hand UTRs - let start - let end - if (!haveLeftUTR) { - for (let i = 0; i < exons.length; i++) { - start = exons[i].get('start') - if (start >= codeStart) { - break - } - end = codeStart > exons[i].get('end') ? exons[i].get('end') : codeStart - const type = strand >= 0 ? 'five_prime_UTR' : 'three_prime_UTR' - subparts.unshift( - new SimpleFeature({ - parent, - id: `${parent.id()}_${type}_${i}`, - data: { start, end, strand, type }, - }), - ) - } - } - - // make the right-hand UTRs - if (!haveRightUTR) { - for (let i = exons.length - 1; i >= 0; i--) { - end = exons[i].get('end') - if (end <= codeEnd) { - break - } - - start = codeEnd < exons[i].get('start') ? exons[i].get('start') : codeEnd - const type = strand >= 0 ? 'three_prime_UTR' : 'five_prime_UTR' - subparts.push( - new SimpleFeature({ - parent, - id: `${parent.id()}_${type}_${i}`, - data: { start, end, strand, type }, - }), - ) - } - } - - return subparts -} - -function getSubparts(f, config) { - let c = f.get('subfeatures') - if (!c || !c.length) { - return [] - } - const hasUTRs = !!c.find(child => isUTR(child)) - const isTranscript = ['mRNA', 'transcript'].includes(f.get('type')) - const impliedUTRs = !hasUTRs && isTranscript - - // if we think we should use impliedUTRs, or it is specifically in the - // config, then makeUTRs - if (impliedUTRs || readConfObject(config, 'impliedUTRs')) { - c = makeUTRs(f, c) - } - - return c.filter(element => filterSubpart(element, config)) -} - -ProcessedTranscript.layOut = ({ - layout, - feature, - bpPerPx, - reversed, - config, -}) => { - const subLayout = layOutFeature({ - layout, - feature, - bpPerPx, - reversed, - config, - }) - const subfeatures = getSubparts(feature, config) - layOutSubfeatures({ - layout: subLayout, - subfeatures, - bpPerPx, - reversed, - config, - }) - return subLayout -} - -export default observer(ProcessedTranscript) diff --git a/plugins/canvas/src/CanvasFeatureRenderer/components/Segments.js b/plugins/canvas/src/CanvasFeatureRenderer/components/Segments.js deleted file mode 100644 index 8a87a412b5..0000000000 --- a/plugins/canvas/src/CanvasFeatureRenderer/components/Segments.js +++ /dev/null @@ -1,104 +0,0 @@ -import { readConfObject } from '@jbrowse/core/configuration' -import { PropTypes as CommonPropTypes } from '@jbrowse/core/util/types/mst' -import { emphasize } from '@jbrowse/core/util/color' -import { observer } from 'mobx-react' -import PropTypes from 'prop-types' -import React from 'react' - -function Segments(props) { - const { - feature, - featureLayout, - selected, - config, - reversed, - // some subfeatures may be computed e.g. makeUTRs, - // so these are passed as a prop - // eslint-disable-next-line react/prop-types - subfeatures: subfeaturesProp, - } = props - - const subfeatures = subfeaturesProp || feature.get('subfeatures') - const color2 = readConfObject(config, 'color2', { feature }) - let emphasizedColor2 - try { - emphasizedColor2 = emphasize(color2, 0.3) - } catch (error) { - emphasizedColor2 = color2 - } - const { left, top, width, height } = featureLayout.absolute - const points = [ - [left, top + height / 2], - [left + width, top + height / 2], - ] - const strand = feature.get('strand') - if (strand) { - points.push( - [left + width - height / 4, top + height / 4], - [left + width - height / 4, top + 3 * (height / 4)], - [left + width, top + height / 2], - ) - } - - return ( - <> - 0)) - ? `rotate(180,${left + width / 2},${top + height / 2})` - : undefined - } - points={points} - stroke={selected ? emphasizedColor2 : color2} - /> - { - // eslint-disable-next-line react/prop-types - subfeatures.map(subfeature => { - const subfeatureId = String(subfeature.id()) - const subfeatureLayout = featureLayout.getSubRecord(subfeatureId) - // This subfeature got filtered out - if (!subfeatureLayout) { - return null - } - const { GlyphComponent } = subfeatureLayout.data - return ( - - ) - }) - } - - ) -} - -Segments.propTypes = { - feature: PropTypes.shape({ - id: PropTypes.func.isRequired, - get: PropTypes.func.isRequired, - }).isRequired, - featureLayout: PropTypes.shape({ - absolute: PropTypes.shape({ - top: PropTypes.number.isRequired, - left: PropTypes.number.isRequired, - width: PropTypes.number.isRequired, - height: PropTypes.number.isRequired, - }), - getSubRecord: PropTypes.func.isRequired, - }).isRequired, - selected: PropTypes.bool, - config: CommonPropTypes.ConfigSchema.isRequired, - reversed: PropTypes.bool, -} - -Segments.defaultProps = { - selected: false, - reversed: false, -} - -export default observer(Segments) diff --git a/plugins/canvas/src/CanvasFeatureRenderer/components/Subfeatures.js b/plugins/canvas/src/CanvasFeatureRenderer/components/Subfeatures.js deleted file mode 100644 index deadbab4c8..0000000000 --- a/plugins/canvas/src/CanvasFeatureRenderer/components/Subfeatures.js +++ /dev/null @@ -1,81 +0,0 @@ -import { readConfObject } from '@jbrowse/core/configuration' -import { observer } from 'mobx-react' -import PropTypes from 'prop-types' -import React from 'react' -import { chooseGlyphComponent, layOut, layOutFeature } from './util' - -function Subfeatures(props) { - const { feature, featureLayout, selected } = props - - return ( - <> - {feature.get('subfeatures').map(subfeature => { - const subfeatureId = String(subfeature.id()) - const subfeatureLayout = featureLayout.getSubRecord(subfeatureId) - const { GlyphComponent } = subfeatureLayout.data - return ( - - ) - })} - - ) -} - -Subfeatures.propTypes = { - feature: PropTypes.shape({ get: PropTypes.func.isRequired }).isRequired, - featureLayout: PropTypes.shape({ - getSubRecord: PropTypes.func.isRequired, - }).isRequired, - selected: PropTypes.bool, - reversed: PropTypes.bool, -} - -Subfeatures.defaultProps = { - selected: false, - reversed: false, -} - -Subfeatures.layOut = ({ layout, feature, bpPerPx, reversed, config }) => { - const subLayout = layOutFeature({ - layout, - feature, - bpPerPx, - reversed, - config, - }) - const displayMode = readConfObject(config, 'displayMode') - if (displayMode !== 'reducedRepresentation') { - const subfeatures = feature.get('subfeatures') || [] - let topOffset = 0 - subfeatures.forEach(subfeature => { - const SubfeatureGlyphComponent = chooseGlyphComponent(subfeature) - const subfeatureHeight = readConfObject(config, 'height', { - feature: subfeature, - }) - - const subSubLayout = (SubfeatureGlyphComponent.layOut || layOut)({ - layout: subLayout, - feature: subfeature, - bpPerPx, - reversed, - config, - }) - subSubLayout.move(0, topOffset) - topOffset += - displayMode === 'collapse' - ? 0 - : (displayMode === 'compact' - ? subfeatureHeight / 3 - : subfeatureHeight) + 2 - }) - } - return subLayout -} - -export default observer(Subfeatures) diff --git a/plugins/canvas/src/CanvasFeatureRenderer/components/SvgFeatureRendering.js b/plugins/canvas/src/CanvasFeatureRenderer/components/SvgFeatureRendering.js deleted file mode 100644 index 5a3e6ff8de..0000000000 --- a/plugins/canvas/src/CanvasFeatureRenderer/components/SvgFeatureRendering.js +++ /dev/null @@ -1,172 +0,0 @@ -import { readConfObject } from '@jbrowse/core/configuration' -import { PropTypes as CommonPropTypes } from '@jbrowse/core/util/types/mst' -import { bpToPx, measureText } from '@jbrowse/core/util' -import SceneGraph from '@jbrowse/core/util/layouts/SceneGraph' -import { observer } from 'mobx-react' -import ReactPropTypes from 'prop-types' -import React, { useEffect, useRef, useState, useCallback } from 'react' -import FeatureGlyph from './FeatureGlyph' -import SvgOverlay from './SvgOverlay' -import { chooseGlyphComponent, layOut } from './util' - -const renderingStyle = { - position: 'relative', -} - -// used to make features have a little padding for their labels -const nameWidthPadding = 2 -const textVerticalPadding = 2 - -// used so that user can click-away-from-feature below the laid out features -// (issue #1248) -const svgHeightPadding = 100 - -function RenderedFeatureGlyph(props) { - const { feature, bpPerPx, region, config, displayMode, layout } = props - const { reversed } = region - const start = feature.get(reversed ? 'end' : 'start') - const startPx = bpToPx(start, region, bpPerPx) - const labelsAllowed = displayMode !== 'compact' && displayMode !== 'collapsed' - - const rootLayout = new SceneGraph('root', 0, 0, 0, 0) - const GlyphComponent = chooseGlyphComponent(feature) - const featureLayout = (GlyphComponent.layOut || layOut)({ - layout: rootLayout, - feature, - bpPerPx, - reversed, - config, - }) - let shouldShowName - let shouldShowDescription - let name - let description - let fontHeight - let expansion - if (labelsAllowed) { - const showLabels = readConfObject(config, 'showLabels') - const showDescriptions = readConfObject(config, 'showDescriptions') - fontHeight = readConfObject(config, ['labels', 'fontSize'], { feature }) - expansion = readConfObject(config, 'maxFeatureGlyphExpansion') || 0 - name = readConfObject(config, ['labels', 'name'], { feature }) || '' - shouldShowName = /\S/.test(name) && showLabels - - description = - readConfObject(config, ['labels', 'description'], { feature }) || '' - shouldShowDescription = - /\S/.test(description) && showLabels && showDescriptions - - let nameWidth = 0 - if (shouldShowName) { - nameWidth = - Math.round( - Math.min(measureText(name, fontHeight), rootLayout.width + expansion), - ) + nameWidthPadding - rootLayout.addChild( - 'nameLabel', - 0, - featureLayout.bottom + textVerticalPadding, - nameWidth, - fontHeight, - ) - } - - let descriptionWidth = 0 - if (shouldShowDescription) { - const aboveLayout = shouldShowName - ? rootLayout.getSubRecord('nameLabel') - : featureLayout - descriptionWidth = - Math.round( - Math.min( - measureText(description, fontHeight), - rootLayout.width + expansion, - ), - ) + nameWidthPadding - rootLayout.addChild( - 'descriptionLabel', - 0, - aboveLayout.bottom + textVerticalPadding, - descriptionWidth, - fontHeight, - ) - } - } - - const topPx = layout.addRect( - feature.id(), - feature.get('start'), - feature.get('start') + rootLayout.width * bpPerPx, - rootLayout.height, - ) - if (topPx === null) { - return null - } - rootLayout.move(startPx, topPx) - - return ( - - ) -} - -RenderedFeatureGlyph.propTypes = { - layout: ReactPropTypes.shape({ - addRect: ReactPropTypes.func.isRequired, - getTotalHeight: ReactPropTypes.func.isRequired, - }).isRequired, - - displayMode: ReactPropTypes.string.isRequired, - region: CommonPropTypes.Region.isRequired, - bpPerPx: ReactPropTypes.number.isRequired, - feature: ReactPropTypes.shape({ - id: ReactPropTypes.func.isRequired, - get: ReactPropTypes.func.isRequired, - }).isRequired, - config: CommonPropTypes.ConfigSchema.isRequired, -} - -const RenderedFeatures = observer(props => { - const { features } = props - const featuresRendered = [] - for (const feature of features.values()) { - featuresRendered.push( - , - ) - } - return <>{featuresRendered} -}) -RenderedFeatures.propTypes = { - features: ReactPropTypes.oneOfType([ - ReactPropTypes.instanceOf(Map), - ReactPropTypes.arrayOf(ReactPropTypes.shape()), - ]), - layout: ReactPropTypes.shape({ - addRect: ReactPropTypes.func.isRequired, - getTotalHeight: ReactPropTypes.func.isRequired, - }).isRequired, -} - -RenderedFeatures.defaultProps = { - features: [], -} - -function SvgFeatureRendering(props) { - return 'hello' -} - -export default observer(SvgFeatureRendering) diff --git a/plugins/canvas/src/CanvasFeatureRenderer/components/SvgFeatureRendering.test.js b/plugins/canvas/src/CanvasFeatureRenderer/components/SvgFeatureRendering.test.js deleted file mode 100644 index 76cee996e4..0000000000 --- a/plugins/canvas/src/CanvasFeatureRenderer/components/SvgFeatureRendering.test.js +++ /dev/null @@ -1,1041 +0,0 @@ -import GranularRectLayout from '@jbrowse/core/util/layouts/GranularRectLayout' -import PrecomputedLayout from '@jbrowse/core/util/layouts/PrecomputedLayout' -import SimpleFeature from '@jbrowse/core/util/simpleFeature' -import React from 'react' -import { render } from '@testing-library/react' -import SvgRendererConfigSchema from '../configSchema' -import Rendering from './SvgFeatureRendering' -import SvgOverlay from './SvgOverlay' - -import '@testing-library/jest-dom/extend-expect' - -test('no features', () => { - const { container } = render( - , - ) - - expect(container.firstChild).toMatchSnapshot() -}) - -test('one feature', () => { - const { container } = render( - , - ) - - expect(container.firstChild).toMatchSnapshot() -}) - -test('one feature (compact mode)', () => { - const config = SvgRendererConfigSchema.create({ displayMode: 'compact' }) - - const { container } = render( - , - ) - - // reducedRepresentation of the transcript is just a box - expect(container.firstChild).toMatchSnapshot() -}) - -test('processed transcript (reducedRepresentation mode)', () => { - const config = SvgRendererConfigSchema.create({ - displayMode: 'reducedRepresentation', - }) - const { container } = render( - , - ) - - expect(container.firstChild).toMatchSnapshot() -}) - -test('processed transcript', () => { - const { container } = render( - , - ) - - expect(container.firstChild).toMatchSnapshot() -}) - -test('processed transcript (exons + impliedUTR)', () => { - const { container } = render( - , - ) - - // finds that the color3 is outputted for impliedUTRs - expect(container).toContainHTML('#357089') - - expect(container.firstChild).toMatchSnapshot() -}) - -// hacks existence of getFeatureByID -test('svg selected', () => { - const { container } = render( - - { - return [0, 0, 10, 10] - }, - featureIdUnderMouse: 'one', - selectedFeatureId: 'one', - }} - config={SvgRendererConfigSchema.create({})} - bpPerPx={3} - /> - , - ) - - expect(container.firstChild).toMatchSnapshot() -}) diff --git a/plugins/canvas/src/CanvasFeatureRenderer/components/SvgOverlay.tsx b/plugins/canvas/src/CanvasFeatureRenderer/components/SvgOverlay.tsx deleted file mode 100644 index 469d2f7ecf..0000000000 --- a/plugins/canvas/src/CanvasFeatureRenderer/components/SvgOverlay.tsx +++ /dev/null @@ -1,247 +0,0 @@ -import { bpSpanPx } from '@jbrowse/core/util' -import SimpleFeature from '@jbrowse/core/util/simpleFeature' -import { Region } from '@jbrowse/core/util/types' -import { observer } from 'mobx-react' -import React from 'react' - -type LayoutRecord = [number, number, number, number] -interface SvgOverlayProps { - region: Region - displayModel: { - getFeatureByID: (arg0: string, arg1: string) => LayoutRecord - selectedFeatureId?: string - featureIdUnderMouse?: string - contextMenuFeature?: SimpleFeature - } - bpPerPx: number - blockKey: string - movedDuringLastMouseDown: boolean - onFeatureMouseDown?( - event: React.MouseEvent, - featureId: string, - ): {} - onFeatureMouseEnter?( - event: React.MouseEvent, - featureId: string, - ): {} - onFeatureMouseOut?( - event: - | React.MouseEvent - | React.FocusEvent, - featureId: string, - ): {} - onFeatureMouseOver?( - event: - | React.MouseEvent - | React.FocusEvent, - featureId: string, - ): {} - onFeatureMouseUp?( - event: React.MouseEvent, - featureId: string, - ): {} - onFeatureMouseLeave?( - event: React.MouseEvent, - featureId: string, - ): {} - onFeatureMouseMove?( - event: React.MouseEvent, - featureId: string, - ): {} - // synthesized from mouseup and mousedown - onFeatureClick?( - event: React.MouseEvent, - featureId: string, - ): {} - onFeatureContextMenu?( - event: React.MouseEvent, - featureId: string, - ): {} -} - -interface OverlayRectProps extends React.SVGProps { - rect?: LayoutRecord - region: Region - bpPerPx: number -} - -function OverlayRect({ - rect, - region, - bpPerPx, - ...rectProps -}: OverlayRectProps) { - if (!rect) { - return null - } - const [leftBp, topPx, rightBp, bottomPx] = rect - const [leftPx, rightPx] = bpSpanPx(leftBp, rightBp, region, bpPerPx) - const rectTop = Math.round(topPx) - const screenWidth = (region.end - region.start) / bpPerPx - const rectHeight = Math.round(bottomPx - topPx) - const width = rightPx - leftPx - - if (leftPx + width < 0) { - return null - } - const leftWithinBlock = Math.max(leftPx, 0) - const diff = leftWithinBlock - leftPx - const widthWithinBlock = Math.max(1, Math.min(width - diff, screenWidth)) - - return ( - - ) -} - -function SvgOverlay({ - displayModel, - blockKey, - region, - bpPerPx, - movedDuringLastMouseDown, - ...handlers -}: SvgOverlayProps) { - const { selectedFeatureId, featureIdUnderMouse, contextMenuFeature } = - displayModel - - const mouseoverFeatureId = featureIdUnderMouse || contextMenuFeature?.id() - - function onFeatureMouseDown( - event: React.MouseEvent, - ) { - const { onFeatureMouseDown: handler } = handlers - if (!(handler && mouseoverFeatureId)) { - return undefined - } - return handler(event, mouseoverFeatureId) - } - - function onFeatureMouseEnter( - event: React.MouseEvent, - ) { - const { onFeatureMouseEnter: handler } = handlers - if (!(handler && mouseoverFeatureId)) { - return undefined - } - return handler(event, mouseoverFeatureId) - } - - function onFeatureMouseOut( - event: - | React.MouseEvent - | React.FocusEvent, - ) { - const { onFeatureMouseOut: handler } = handlers - if (!(handler && mouseoverFeatureId)) { - return undefined - } - return handler(event, mouseoverFeatureId) - } - - function onFeatureMouseOver( - event: - | React.MouseEvent - | React.FocusEvent, - ) { - const { onFeatureMouseOver: handler } = handlers - if (!(handler && mouseoverFeatureId)) { - return undefined - } - return handler(event, mouseoverFeatureId) - } - - function onFeatureMouseUp( - event: React.MouseEvent, - ) { - const { onFeatureMouseUp: handler } = handlers - if (!(handler && mouseoverFeatureId)) { - return undefined - } - return handler(event, mouseoverFeatureId) - } - - function onFeatureMouseLeave( - event: React.MouseEvent, - ) { - const { onFeatureMouseLeave: handler } = handlers - if (!(handler && mouseoverFeatureId)) { - return undefined - } - return handler(event, mouseoverFeatureId) - } - - function onFeatureMouseMove( - event: React.MouseEvent, - ) { - const { onFeatureMouseMove: handler } = handlers - if (!(handler && mouseoverFeatureId)) { - return undefined - } - return handler(event, mouseoverFeatureId) - } - - function onFeatureClick(event: React.MouseEvent) { - if (movedDuringLastMouseDown) { - return undefined - } - const { onFeatureClick: handler } = handlers - if (!(handler && mouseoverFeatureId)) { - return undefined - } - event.stopPropagation() - return handler(event, mouseoverFeatureId) - } - - function onFeatureContextMenu( - event: React.MouseEvent, - ) { - const { onFeatureContextMenu: handler } = handlers - if (!(handler && mouseoverFeatureId)) { - return undefined - } - return handler(event, mouseoverFeatureId) - } - - return ( - <> - {mouseoverFeatureId ? ( - - ) : null} - {selectedFeatureId ? ( - - ) : null} - - ) -} - -export default observer(SvgOverlay) diff --git a/plugins/canvas/src/CanvasFeatureRenderer/components/__snapshots__/SvgFeatureRendering.test.js.snap b/plugins/canvas/src/CanvasFeatureRenderer/components/__snapshots__/SvgFeatureRendering.test.js.snap deleted file mode 100644 index 9bb0eb1feb..0000000000 --- a/plugins/canvas/src/CanvasFeatureRenderer/components/__snapshots__/SvgFeatureRendering.test.js.snap +++ /dev/null @@ -1,585 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`no features 1`] = ` -
- -
-`; - -exports[`one feature (compact mode) 1`] = ` -
- - - - - - - - - - - - - - - - - - - -
-`; - -exports[`one feature 1`] = ` -
- - - - - -
-`; - -exports[`processed transcript (exons + impliedUTR) 1`] = ` -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - LACTBL1 - - - lactamase beta like 1 - - - -
-`; - -exports[`processed transcript (reducedRepresentation mode) 1`] = ` -
- - - - - -
-`; - -exports[`processed transcript 1`] = ` -
- - - - - - - - - - - - - - - - - - - au9.g1002.t1 - - - -
-`; - -exports[`svg selected 1`] = ` - - - - -`; diff --git a/plugins/canvas/src/CanvasFeatureRenderer/components/util.ts b/plugins/canvas/src/CanvasFeatureRenderer/components/util.ts deleted file mode 100644 index c495a583db..0000000000 --- a/plugins/canvas/src/CanvasFeatureRenderer/components/util.ts +++ /dev/null @@ -1,129 +0,0 @@ -import React from 'react' -import { readConfObject } from '@jbrowse/core/configuration' -import SceneGraph from '@jbrowse/core/util/layouts/SceneGraph' -import { Feature } from '@jbrowse/core/util/simpleFeature' -import { AnyConfigurationModel } from '@jbrowse/core/configuration/configurationSchema' -import Box from './Box' -import Chevron from './Chevron' -import ProcessedTranscript from './ProcessedTranscript' -import Segments from './Segments' -import Subfeatures from './Subfeatures' - -interface Glyph extends React.FunctionComponent { - layOut?: Function -} - -export function chooseGlyphComponent(feature: Feature): Glyph { - const type = feature.get('type') - const strand = feature.get('strand') - const subfeatures: Feature[] = feature.get('subfeatures') - - if (subfeatures) { - const hasSubSub = subfeatures.find(subfeature => { - return !!subfeature.get('subfeatures') - }) - if (hasSubSub) { - return Subfeatures - } - const transcriptTypes = ['mRNA', 'transcript'] - if ( - transcriptTypes.includes(type) && - subfeatures.find(f => f.get('type') === 'CDS') - ) { - return ProcessedTranscript - } - return Segments - } - return [1, -1].includes(strand) ? Chevron : Box -} - -interface BaseLayOutArgs { - layout: SceneGraph - bpPerPx: number - reversed: boolean - config: AnyConfigurationModel -} - -interface FeatureLayOutArgs extends BaseLayOutArgs { - feature: Feature -} - -interface SubfeatureLayOutArgs extends BaseLayOutArgs { - subfeatures: Feature[] -} - -export function layOut({ - layout, - feature, - bpPerPx, - reversed, - config, -}: FeatureLayOutArgs): SceneGraph { - const displayMode = readConfObject(config, 'displayMode') - const subLayout = layOutFeature({ - layout, - feature, - bpPerPx, - reversed, - config, - }) - if (displayMode !== 'reducedRepresentation') { - layOutSubfeatures({ - layout: subLayout, - subfeatures: feature.get('subfeatures') || [], - bpPerPx, - reversed, - config, - }) - } - return subLayout -} - -export function layOutFeature(args: FeatureLayOutArgs): SceneGraph { - const { layout, feature, bpPerPx, reversed, config } = args - const displayMode = readConfObject(config, 'displayMode') - const GlyphComponent = - displayMode === 'reducedRepresentation' - ? Chevron - : chooseGlyphComponent(feature) - const parentFeature = feature.parent() - let x = 0 - if (parentFeature) { - x = reversed - ? (parentFeature.get('end') - feature.get('end')) / bpPerPx - : (feature.get('start') - parentFeature.get('start')) / bpPerPx - } - const height = readConfObject(config, 'height', { feature }) - const width = (feature.get('end') - feature.get('start')) / bpPerPx - const layoutParent = layout.parent - const top = layoutParent ? layoutParent.top : 0 - const subLayout = layout.addChild( - String(feature.id()), - x, - displayMode === 'collapse' ? 0 : top, - width, - displayMode === 'compact' ? height / 2 : height, - { GlyphComponent }, - ) - return subLayout -} - -export function layOutSubfeatures(args: SubfeatureLayOutArgs): void { - const { layout: subLayout, subfeatures, bpPerPx, reversed, config } = args - subfeatures.forEach(subfeature => { - const SubfeatureGlyphComponent = chooseGlyphComponent(subfeature) - ;(SubfeatureGlyphComponent.layOut || layOut)({ - layout: subLayout, - feature: subfeature, - bpPerPx, - reversed, - config, - }) - }) -} - -export function isUTR(feature: Feature): boolean { - return /(\bUTR|_UTR|untranslated[_\s]region)\b/.test( - feature.get('type') || '', - ) -} diff --git a/plugins/canvas/src/CanvasFeatureRenderer/configSchema.ts b/plugins/canvas/src/CanvasFeatureRenderer/configSchema.ts index b4fdc3ad31..0d21e6f049 100644 --- a/plugins/canvas/src/CanvasFeatureRenderer/configSchema.ts +++ b/plugins/canvas/src/CanvasFeatureRenderer/configSchema.ts @@ -2,7 +2,7 @@ import { ConfigurationSchema } from '@jbrowse/core/configuration' import { types } from 'mobx-state-tree' export default ConfigurationSchema( - 'SvgFeatureRenderer', + 'CanvasFeatureRenderer', { color1: { type: 'color', @@ -38,7 +38,7 @@ export default ConfigurationSchema( type: 'boolean', defaultValue: true, }, - labels: ConfigurationSchema('SvgFeatureLabels', { + labels: ConfigurationSchema('CanvasFeatureLabels', { name: { type: 'string', description: @@ -91,7 +91,7 @@ export default ConfigurationSchema( }, maxHeight: { type: 'integer', - description: 'the maximum height to be used in a svg rendering', + description: 'the maximum height to be used in a canvas rendering', defaultValue: 600, }, subParts: { diff --git a/plugins/canvas/src/CanvasFeatureRenderer/index.ts b/plugins/canvas/src/CanvasFeatureRenderer/index.ts index 0f8629b999..62e37a7ab1 100644 --- a/plugins/canvas/src/CanvasFeatureRenderer/index.ts +++ b/plugins/canvas/src/CanvasFeatureRenderer/index.ts @@ -1,2 +1,2 @@ -export { default as ReactComponent } from './components/SvgFeatureRendering' +export { default as ReactComponent } from './components/CanvasFeatureRendering' export { default as configSchema } from './configSchema' diff --git a/plugins/linear-genome-view/src/LinearBasicDisplay/model.ts b/plugins/linear-genome-view/src/LinearBasicDisplay/model.ts index 6fb470d792..d4b42b5709 100644 --- a/plugins/linear-genome-view/src/LinearBasicDisplay/model.ts +++ b/plugins/linear-genome-view/src/LinearBasicDisplay/model.ts @@ -2,9 +2,13 @@ import { lazy } from 'react' import { ConfigurationReference, getConf } from '@jbrowse/core/configuration' import { getSession } from '@jbrowse/core/util' import { MenuItem } from '@jbrowse/core/ui' -import VisibilityIcon from '@material-ui/icons/Visibility' import { types, getEnv, Instance } from 'mobx-state-tree' import { AnyConfigurationSchemaType } from '@jbrowse/core/configuration/configurationSchema' + +// icons +import VisibilityIcon from '@material-ui/icons/Visibility' + +// locals import { BaseLinearDisplay } from '../BaseLinearDisplay' const SetMaxHeightDlg = lazy(() => import('./components/SetMaxHeight')) @@ -30,41 +34,41 @@ const stateModelFactory = (configSchema: AnyConfigurationSchemaType) => get showLabels() { const showLabels = getConf(self, ['renderer', 'showLabels']) - return self.trackShowLabels !== undefined - ? self.trackShowLabels - : showLabels + const { trackShowLabels } = self + return trackShowLabels !== undefined ? trackShowLabels : showLabels }, get showDescriptions() { const showDescriptions = getConf(self, ['renderer', 'showLabels']) - return self.trackShowDescriptions !== undefined - ? self.trackShowDescriptions + const { trackShowDescriptions } = self + return trackShowDescriptions !== undefined + ? trackShowDescriptions : showDescriptions }, get maxHeight() { const maxHeight = getConf(self, ['renderer', 'maxHeight']) - return self.trackMaxHeight !== undefined - ? self.trackMaxHeight - : maxHeight + const { trackMaxHeight } = self + return trackMaxHeight !== undefined ? trackMaxHeight : maxHeight }, get displayMode() { const displayMode = getConf(self, ['renderer', 'displayMode']) - return self.trackDisplayMode !== undefined - ? self.trackDisplayMode - : displayMode + const { trackDisplayMode } = self + return trackDisplayMode !== undefined ? trackDisplayMode : displayMode }, + })) + .views(self => ({ get rendererConfig() { const configBlob = getConf(self, ['renderer']) || {} return self.rendererType.configSchema.create( { ...configBlob, - showLabels: this.showLabels, - showDescriptions: this.showDescriptions, - displayMode: this.displayMode, - maxHeight: this.maxHeight, + showLabels: self.showLabels, + showDescriptions: self.showDescriptions, + displayMode: self.displayMode, + maxHeight: self.maxHeight, }, getEnv(self), ) @@ -92,12 +96,9 @@ const stateModelFactory = (configSchema: AnyConfigurationSchemaType) => } = self return { renderProps() { - const config = self.rendererConfig - console.log(config) - return { ...superRenderProps(), - config, + config: self.rendererConfig, } }, @@ -140,7 +141,7 @@ const stateModelFactory = (configSchema: AnyConfigurationSchemaType) => { label: 'Set max height', onClick: () => { - getSession(self).queueDialog((doneCallback: Function) => [ + getSession(self).queueDialog(doneCallback => [ SetMaxHeightDlg, { model: self, handleClose: doneCallback }, ]) From b0726cb74dcd182ab3b7c813d08dd2b5bd07f520 Mon Sep 17 00:00:00 2001 From: Colin Date: Sat, 23 Oct 2021 16:34:49 -0400 Subject: [PATCH 06/28] Incremental Use color Box glyph Misc Back to main updates Updates Incremental Canvas glyph types Misc Label Remove updateStaticElement Correct positioning Start rendering text System for rendering labels at proper place Crazy code Misc Updates Testing --- packages/core/util/offscreenCanvasPonyfill.js | 5 +- packages/core/util/offscreenCanvasUtils.tsx | 4 +- .../CanvasFeatureRenderer.tsx | 214 +++++++++++++++ .../src/CanvasFeatureRenderer/FeatureGlyph.ts | 70 +++++ .../FeatureGlyphs/Box.ts | 165 ++++++++++++ .../FeatureGlyphs/Gene.ts | 119 ++++++++ .../FeatureGlyphs/ProcessedTranscript.ts | 233 ++++++++++++++++ .../FeatureGlyphs/Segments.ts | 68 +++++ .../FeatureGlyphs/UnprocessedTranscript.ts | 15 ++ .../components/CanvasFeatureRendering.js | 8 - .../components/CanvasFeatureRendering.tsx | 255 ++++++++++++++++++ .../canvas/src/CanvasFeatureRenderer/index.ts | 3 + plugins/canvas/src/index.ts | 7 +- .../components/ExportSvgDialog.tsx | 2 +- .../components/SvgFeatureRendering.js | 180 ++----------- .../components/SvgOverlay.tsx | 204 +++++++++++++- test_data/config_demo.json | 14 +- 17 files changed, 1381 insertions(+), 185 deletions(-) create mode 100644 plugins/canvas/src/CanvasFeatureRenderer/CanvasFeatureRenderer.tsx create mode 100644 plugins/canvas/src/CanvasFeatureRenderer/FeatureGlyph.ts create mode 100644 plugins/canvas/src/CanvasFeatureRenderer/FeatureGlyphs/Box.ts create mode 100644 plugins/canvas/src/CanvasFeatureRenderer/FeatureGlyphs/Gene.ts create mode 100644 plugins/canvas/src/CanvasFeatureRenderer/FeatureGlyphs/ProcessedTranscript.ts create mode 100644 plugins/canvas/src/CanvasFeatureRenderer/FeatureGlyphs/Segments.ts create mode 100644 plugins/canvas/src/CanvasFeatureRenderer/FeatureGlyphs/UnprocessedTranscript.ts delete mode 100644 plugins/canvas/src/CanvasFeatureRenderer/components/CanvasFeatureRendering.js create mode 100644 plugins/canvas/src/CanvasFeatureRenderer/components/CanvasFeatureRendering.tsx diff --git a/packages/core/util/offscreenCanvasPonyfill.js b/packages/core/util/offscreenCanvasPonyfill.js index 17028d991c..8ffbf6f8f3 100644 --- a/packages/core/util/offscreenCanvasPonyfill.js +++ b/packages/core/util/offscreenCanvasPonyfill.js @@ -97,11 +97,14 @@ export class PonyfillOffscreenContext { fillRect(...args) { const [x, y, w, h] = args + + // avoid rendering offscreen contents if (x > this.width || x + w < 0) { return } + const nx = Math.max(x, 0) - const nw = nx + w > this.width ? this.width - nx : w + const nw = w - (nx - x) this.commands.push({ type: 'fillRect', args: [nx, y, nw, h] }) } diff --git a/packages/core/util/offscreenCanvasUtils.tsx b/packages/core/util/offscreenCanvasUtils.tsx index b05a1076b1..088f30f91f 100644 --- a/packages/core/util/offscreenCanvasUtils.tsx +++ b/packages/core/util/offscreenCanvasUtils.tsx @@ -13,12 +13,14 @@ export async function renderToAbstractCanvas( exportSVG?: { rasterizeLayers?: boolean } highResolutionScaling: number }, - cb: Function, + cb: (arg: CanvasRenderingContext2D) => Promise | void, ) { const { exportSVG, highResolutionScaling = 1 } = opts if (exportSVG && !exportSVG.rasterizeLayers) { const fakeCanvas = new PonyfillOffscreenCanvas(width, height) const fakeCtx = fakeCanvas.getContext('2d') + + // @ts-ignore just assume we are actually rendering to a canvas await cb(fakeCtx) return { reactElement: fakeCanvas.getSerializedSvg(), diff --git a/plugins/canvas/src/CanvasFeatureRenderer/CanvasFeatureRenderer.tsx b/plugins/canvas/src/CanvasFeatureRenderer/CanvasFeatureRenderer.tsx new file mode 100644 index 0000000000..69a043396c --- /dev/null +++ b/plugins/canvas/src/CanvasFeatureRenderer/CanvasFeatureRenderer.tsx @@ -0,0 +1,214 @@ +import BoxRendererType, { + RenderArgs, + RenderArgsSerialized, + RenderArgsDeserialized as BoxRenderArgsDeserialized, + RenderResults, + ResultsSerialized, + ResultsDeserialized, +} from '@jbrowse/core/pluggableElementTypes/renderers/BoxRendererType' +import { Feature } from '@jbrowse/core/util/simpleFeature' +import { BaseLayout } from '@jbrowse/core/util/layouts/BaseLayout' +import { iterMap, bpSpanPx } from '@jbrowse/core/util' +import { renderToAbstractCanvas } from '@jbrowse/core/util/offscreenCanvasUtils' +import BoxGlyph from './FeatureGlyphs/Box' +import GeneGlyph from './FeatureGlyphs/Gene' +import { LaidOutFeatureRect } from './FeatureGlyph' + +export type { + RenderArgs, + RenderArgsSerialized, + RenderResults, + ResultsSerialized, + ResultsDeserialized, +} + +export interface RenderArgsDeserialized extends BoxRenderArgsDeserialized { + highResolutionScaling: number +} + +export interface RenderArgsDeserializedWithFeaturesAndLayout + extends RenderArgsDeserialized { + features: Map + layout: BaseLayout + regionSequence?: string + width: number + height: number +} + +export default class CanvasRenderer extends BoxRendererType { + supportsSVG = true + + layoutFeature( + feature: Feature, + layout: BaseLayout, + props: RenderArgsDeserialized, + ): LaidOutFeatureRect | null { + const glyph = new BoxGlyph() + const geneglyph = new GeneGlyph() + const [region] = props.regions + if (feature.get('type') === 'gene') { + return geneglyph.layoutFeature({ region, ...props }, layout, feature) + } else { + return glyph.layoutFeature({ region, ...props }, layout, feature) + } + } + + drawRect( + ctx: CanvasRenderingContext2D, + fRect: LaidOutFeatureRect, + props: RenderArgsDeserialized, + ) { + const { regions } = props + const [region] = regions + + const glyph = new BoxGlyph() + const geneglyph = new GeneGlyph() + + if (fRect.f.get('type') === 'gene') { + geneglyph.renderFeature(ctx, { ...props, region }, fRect) + } else { + glyph.renderFeature(ctx, { ...props, region }, fRect) + } + } + + async makeImageData( + ctx: CanvasRenderingContext2D, + layoutRecords: LaidOutFeatureRect[], + props: RenderArgsDeserializedWithFeaturesAndLayout, + ) { + layoutRecords + .filter(f => !!f) + .forEach(fRect => { + this.drawRect(ctx, fRect, props) + }) + + if (props.exportSVG) { + // console.log( + // bpSpanPx( + // props.regions[0].start, + // props.regions[0].start + 1, + // props.regions[0], + // props.bpPerPx, + // )[0], + // ) + postDraw({ + ctx, + layoutRecords: layoutRecords.map(f => ({ + label: f.label, + description: f.description, + l: f.l, + t: f.t, + start: f.f.get('start'), + end: f.f.get('end'), + })), + offsetPx: 0, + ...props, + }) + } + } + + async render(renderProps: RenderArgsDeserialized) { + const { bpPerPx, regions } = renderProps + const features = await this.getFeatures(renderProps) + const layout = this.createLayoutInWorker(renderProps) + const [region] = regions + const featureMap = features + const layoutRecords = iterMap( + featureMap.values(), + feature => this.layoutFeature(feature, layout, renderProps), + featureMap.size, + ).filter((f): f is LaidOutFeatureRect => !!f) + const width = (region.end - region.start) / bpPerPx + const height = Math.max(layout.getTotalHeight(), 1) + + const res = await renderToAbstractCanvas(width, height, renderProps, ctx => + this.makeImageData(ctx, layoutRecords, { + ...renderProps, + layout, + features, + width, + height, + }), + ) + + const results = await super.render({ + ...renderProps, + ...res, + layoutRecords, + features, + layout, + height, + width, + }) + + return { + ...results, + ...res, + layoutRecords: layoutRecords.map(f => ({ + label: f.label, + description: f.description, + l: f.l, + t: f.t, + start: f.f.get('start'), + end: f.f.get('end'), + })), + features, + layout, + height, + width, + maxHeightReached: layout.maxHeightReached, + } + } +} + +export function postDraw({ + ctx, + layoutRecords, + offsetPx, + regions, +}: { + ctx: CanvasRenderingContext2D + regions: { start: number }[] + offsetPx: number + layoutRecords: any +}) { + const [region] = regions + + ctx.fillStyle = 'black' + ctx.font = '9px sans-serif' + layoutRecords + .filter(f => !!f) + .forEach(record => { + const { + start, + end, + l, + t, + label: { text, yOffset }, + } = record + + if (start < region.start && region.start < end) { + ctx.fillText(text, offsetPx, t + yOffset) + } else { + ctx.fillText(text, l, t + yOffset) + } + }) + + ctx.fillStyle = 'blue' + layoutRecords + .filter(f => !!f) + .forEach(record => { + const { + start, + end, + l, + t, + description: { text, yOffset }, + } = record + if (start < region.start && region.start < end) { + ctx.fillText(text, offsetPx, t + yOffset) + } else { + ctx.fillText(text, l, t + yOffset) + } + }) +} diff --git a/plugins/canvas/src/CanvasFeatureRenderer/FeatureGlyph.ts b/plugins/canvas/src/CanvasFeatureRenderer/FeatureGlyph.ts new file mode 100644 index 0000000000..6367da101c --- /dev/null +++ b/plugins/canvas/src/CanvasFeatureRenderer/FeatureGlyph.ts @@ -0,0 +1,70 @@ +import { readConfObject } from '@jbrowse/core/configuration' +import { Feature } from '@jbrowse/core/util/simpleFeature' +import { AnyConfigurationModel } from '@jbrowse/core/configuration/configurationSchema' +import { bpSpanPx } from '@jbrowse/core/util' +import { Region } from '@jbrowse/core/util/types' + +export interface ViewInfo { + bpPerPx: number + region: Region + config: AnyConfigurationModel +} + +export interface FeatureRect { + l: number + w: number + f: Feature +} + +export interface LaidOutFeatureRect extends FeatureRect { + t: number + h: number +} + +export default class FeatureGlyph { + layoutFeature(viewArgs: ViewInfo, layout: any, feature: Feature) { + const fRect = this.getFeatureRectangle(viewArgs, feature) + + const { region, bpPerPx } = viewArgs + const scale = 1 / bpPerPx + const leftBase = region.start + const startbp = fRect.l / scale + leftBase + const endbp = (fRect.l + fRect.w) / scale + leftBase + const top = layout.addRect(feature.id(), startbp, endbp, fRect.h, feature) + if (top === null) { + return null + } + + return { ...fRect, f: feature, t: top } + } + + // stub + renderFeature( + context: CanvasRenderingContext2D, + viewInfo: ViewInfo, + fRect: LaidOutFeatureRect, + ) {} + + getFeatureRectangle(viewInfo: ViewInfo, feature: Feature) { + const { region, bpPerPx } = viewInfo + const [leftPx, rightPx] = bpSpanPx( + feature.get('start'), + feature.get('end'), + region, + bpPerPx, + ) + + return { + l: leftPx, + w: rightPx - leftPx, + h: this.getFeatureHeight(viewInfo, feature), + f: feature, + t: 0, + } + } + + getFeatureHeight(viewInfo: ViewInfo, feature: Feature) { + const { config } = viewInfo + return readConfObject(config, 'height', { feature }) + } +} diff --git a/plugins/canvas/src/CanvasFeatureRenderer/FeatureGlyphs/Box.ts b/plugins/canvas/src/CanvasFeatureRenderer/FeatureGlyphs/Box.ts new file mode 100644 index 0000000000..5f620241e9 --- /dev/null +++ b/plugins/canvas/src/CanvasFeatureRenderer/FeatureGlyphs/Box.ts @@ -0,0 +1,165 @@ +import { measureText } from '@jbrowse/core/util' +import { Feature } from '@jbrowse/core/util/simpleFeature' +import { readConfObject } from '@jbrowse/core/configuration' +import { bpSpanPx } from '@jbrowse/core/util' + +// locals +import FeatureGlyph, { + ViewInfo, + FeatureRect, + LaidOutFeatureRect, +} from '../FeatureGlyph' + +export default class Box extends FeatureGlyph { + makeFeatureLabel(feature: Feature, fRect: FeatureRect, param?: string) { + const text = param || this.getFeatureLabel(feature) + return this.makeBottomOrTopLabel(text, fRect) + } + + getFeatureLabel(feature: Feature) { + return feature.get('name') || feature.get('id') + } + + getFeatureDescription(feature: Feature) { + return feature.get('description') || feature.get('note') + } + + makeFeatureDescriptionLabel( + feature: Feature, + fRect: FeatureRect, + param?: string, + ) { + const text = param || this.getFeatureDescription(feature) + return this.makeBottomOrTopLabel(text, fRect) + } + + makeSideLabel(text: string, fRect: FeatureRect) { + if (text.length > 100) { + text = text.slice(0, 100) + '…' + } + + return { + text: text, + baseline: 'middle', + w: measureText(text), + h: 10, // FIXME + xOffset: 0, + yOffset: 0, + } + } + + makeBottomOrTopLabel(text = '', fRect: FeatureRect) { + if (text.length > 100) { + text = text.slice(0, 100) + '…' + } + return { + text: text, + w: measureText(text), + h: 10, // FIXME + yOffset: 0, + } + } + + getFeatureRectangle(viewArgs: ViewInfo, feature: Feature) { + const fRect = super.getFeatureRectangle(viewArgs, feature) + + const { l, h } = fRect + const w = Math.max(fRect.w, 2) + fRect.rect = { h, l, w, t: 0 } + fRect.w = w + + // fixme maybe + // const i = { width: 16 } + // if (strand === -1) { + // fRect.w += i.width + // fRect.l -= i.width + // } else if (strand === 1) { + // fRect.w += i.width + // } + + return this.expandRectangleWithLabels(viewArgs, feature, fRect) + } + + // given an under-construction feature layout rectangle, expand it to + // accomodate a label and/or a description + expandRectangleWithLabels( + viewInfo: ViewInfo, + feature: Feature, + fRect: FeatureRect & { + h: number + rect?: { l: number; w: number; h: number } + }, + ) { + // maybe get the feature's name, and update the layout box + // accordingly + const { config } = viewInfo + const showLabels = readConfObject(config, 'showLabels') + const label = showLabels ? this.makeFeatureLabel(feature, fRect) : undefined + if (label) { + fRect.h += label.h + fRect.w = Math.max(label.w, fRect.w) + label.yOffset = fRect.h + } + // maybe get the feature's description if available, and + // update the layout box accordingly + const showDescriptions = readConfObject(config, 'showDescriptions') + const description = showDescriptions + ? this.makeFeatureDescriptionLabel(feature, fRect) + : undefined + if (description) { + fRect.h += description.h + fRect.w = Math.max(description.w, fRect.w) + description.yOffset = fRect.h // -marginBottom removed in jb2 + } + return { ...fRect, description, label } + } + + _embeddedImages = { + plusArrow: { + data: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAkAAAAFCAYAAACXU8ZrAAAATUlEQVQIW2NkwATGQKFYIG4A4g8gacb///+7AWlBmNq+vj6V4uLiJiD/FRBXA/F8xu7u7kcVFRWyMEVATQz//v0Dcf9CxaYRZxIxbgIARiAhmifVe8UAAAAASUVORK5CYII=', + width: 9, + height: 5, + }, + + minusArrow: { + data: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAkAAAAFCAYAAACXU8ZrAAAASklEQVQIW2NkQAABILMBiBcD8VkkcQZGIAeEE4G4FYjFent764qKiu4gKXoPUjAJiLOggsxMTEwMjIwgYQjo6Oh4TLRJME043QQA+W8UD/sdk9IAAAAASUVORK5CYII=', + width: 9, + height: 5, + }, + } + + renderFeature( + context: CanvasRenderingContext2D, + viewInfo: ViewInfo, + fRect: LaidOutFeatureRect & { rect: { h: number } }, + ) { + this.renderBox(context, viewInfo, fRect.f, fRect.t, fRect.rect.h) + } + + // top and height are in px + renderBox( + context: CanvasRenderingContext2D, + viewInfo: ViewInfo, + feature: Feature, + top: number, + overallHeight: number, + ) { + const { config, region, bpPerPx } = viewInfo + const [leftPx, rightPx] = bpSpanPx( + feature.get('start'), + feature.get('end'), + region, + bpPerPx, + ) + const left = leftPx + const width = rightPx - leftPx + + const height = this.getFeatureHeight(viewInfo, feature) + if (height !== overallHeight) { + top += Math.round((overallHeight - height) / 2) + } + + context.fillStyle = readConfObject(config, 'color1', { feature }) + context.fillRect(left, top, Math.max(1, width), height) + } +} diff --git a/plugins/canvas/src/CanvasFeatureRenderer/FeatureGlyphs/Gene.ts b/plugins/canvas/src/CanvasFeatureRenderer/FeatureGlyphs/Gene.ts new file mode 100644 index 0000000000..9f3dde4228 --- /dev/null +++ b/plugins/canvas/src/CanvasFeatureRenderer/FeatureGlyphs/Gene.ts @@ -0,0 +1,119 @@ +import { Feature } from '@jbrowse/core/util/simpleFeature' + +// locals +import BoxGlyph from './Box' +import NoncodingGlyph from './UnprocessedTranscript' +import ProcessedTranscriptGlyph from './ProcessedTranscript' +import { ViewInfo, LaidOutFeatureRect } from '../FeatureGlyph' + +interface FeatureRectWithGlyph extends LaidOutFeatureRect { + label: any + rect: { h: number } +} +interface LaidOutFeatureRectWithSubRects extends LaidOutFeatureRect { + subRects?: FeatureRectWithGlyph[] +} + +export default class Gene extends BoxGlyph { + getFeatureRectangle(viewArgs: ViewInfo, feature: Feature) { + // we need to lay out rects for each of the subfeatures + const subArgs = viewArgs + // subArgs.showDescriptions = false + // subArgs.showLabels = false + const subfeatures = feature.children() + + // if this gene weirdly has no subfeatures, just render as a box + if (!subfeatures?.length) { + return super.getFeatureRectangle(viewArgs, feature) + } + + // get the rects for the children + let padding = 1 + const fRect = { + l: 0, + h: 0, + r: 0, + w: 0, + subRects: [] as FeatureRectWithGlyph[], + f: feature, + t: 0, + } + // sort the children by name + const label = (f: Feature) => f.get('name') || f.get('id') || '' + subfeatures?.sort((a, b) => label(a).localeCompare(label(b))) + + fRect.l = Infinity + fRect.r = -Infinity + + subfeatures.forEach((sub, i) => { + const glyph = this.getSubGlyph(sub) + const subRect = glyph.getFeatureRectangle(subArgs, sub) + + padding = i === subfeatures.length - 1 ? 0 : 1 + const newTop = fRect.h ? fRect.h + padding : 0 + subRect.t = newTop + subRect.rect.t = newTop + + // const transcriptLabel = this.makeSideLabel( + // this.getFeatureLabel(sub), + // subRect, + // ) + + // if (transcriptLabel) { + // subRect.l -= transcriptLabel.w + // subRect.w += transcriptLabel.w + // if (transcriptLabel.h > subRect.h) { + // subRect.h = transcriptLabel.h + // } + // transcriptLabel.yOffset = Math.floor(subRect.h / 2) + // transcriptLabel.xOffset = 0 + // } + + fRect.subRects.push(subRect) // { ...subRect, label: transcriptLabel }) + fRect.r = Math.max(fRect.r, subRect.l + subRect.w - 1) + fRect.l = Math.min(fRect.l, subRect.l) + fRect.h = subRect.t + subRect.rect.h + padding + }) + + // calculate the width + fRect.w = Math.max(fRect.r - fRect.l + 1, 2) + + // expand the fRect to accommodate labels if necessary + return this.expandRectangleWithLabels(viewArgs, feature, fRect) + } + + layoutFeature(viewInfo: ViewInfo, layout: any, feature: Feature) { + const fRect = super.layoutFeature( + viewInfo, + layout, + feature, + ) as LaidOutFeatureRectWithSubRects + + fRect?.subRects?.forEach(subRect => { + subRect.t += fRect.t + subRect.rect.t += fRect.t + }) + return fRect + } + + getSubGlyph(feature: Feature) { + const transcriptType = 'mRNA' + const noncodingType = ['transcript'] + const subType = feature.get('type') + return subType === transcriptType + ? new ProcessedTranscriptGlyph() + : noncodingType.includes(subType) + ? new NoncodingGlyph() + : new BoxGlyph() + } + renderFeature( + context: CanvasRenderingContext2D, + viewInfo: ViewInfo, + fRect: LaidOutFeatureRectWithSubRects, + ) { + fRect.subRects?.forEach(sub => { + const glyph = this.getSubGlyph(sub.f) + glyph.renderFeature(context, viewInfo, sub) + }) + } +} diff --git a/plugins/canvas/src/CanvasFeatureRenderer/FeatureGlyphs/ProcessedTranscript.ts b/plugins/canvas/src/CanvasFeatureRenderer/FeatureGlyphs/ProcessedTranscript.ts new file mode 100644 index 0000000000..c26ddd0e77 --- /dev/null +++ b/plugins/canvas/src/CanvasFeatureRenderer/FeatureGlyphs/ProcessedTranscript.ts @@ -0,0 +1,233 @@ +import SegmentsGlyph from './Segments' +import SimpleFeature, { Feature } from '@jbrowse/core/util/simpleFeature' +import { ViewInfo } from '../FeatureGlyph' + +export default class ProcessedTranscript extends SegmentsGlyph { + protected getSubparts(f: Feature) { + const c = f.children() + if (!c) { + return [] + } + + // if (c && this.config.inferCdsParts) { + // c = this.makeCDSs(f, c) + // } + + // if (c && this.config.impliedUTRs) { + // c = this.makeUTRs(f, c) + // } + + return c + } + + protected makeCDSs(parent: Feature, subparts: Feature[]) { + // infer CDS parts from exon coordinates + + let codeStart = Infinity, + codeEnd = -Infinity + + // gather exons, find coding start and end + let type + const codeIndices = [] + const exons = [] + for (let i = 0; i < subparts.length; i++) { + type = subparts[i].get('type') + if (/^cds/i.test(type)) { + // if any CDSs parts are present already, + // bail and return all subparts as-is + if (/:CDS:/i.test(subparts[i].get('name'))) { + return subparts + } + + codeIndices.push(i) + if (codeStart > subparts[i].get('start')) { + codeStart = subparts[i].get('start') + } + if (codeEnd < subparts[i].get('end')) { + codeEnd = subparts[i].get('end') + } + } else { + if (/exon/i.test(type)) { + exons.push(subparts[i]) + } + } + } + + // splice out unspliced cds parts + codeIndices.sort((a, b) => { + return b - a + }) + for (let i = codeIndices.length - 1; i >= 0; i--) { + subparts.splice(codeIndices[i], 1) + } + + // bail if we don't have exons and cds + if (!(exons.length && codeStart < Infinity && codeEnd > -Infinity)) { + return subparts + } + + // make sure the exons are sorted by coord + exons.sort((a, b) => a.get('start') - b.get('start')) + + // iterate thru exons again, and calculate cds parts + const strand = parent.get('strand') + let codePartStart = Infinity + let codePartEnd = -Infinity + for (let i = 0; i < exons.length; i++) { + const start = exons[i].get('start') + const end = exons[i].get('end') + + // CDS containing exon + if (codeStart >= start && codeEnd <= end) { + codePartStart = codeStart + codePartEnd = codeEnd + } + // 5' terminal CDS part + else if (codeStart >= start && codeStart < end) { + codePartStart = codeStart + codePartEnd = end + } + // 3' terminal CDS part + else if (codeEnd > start && codeEnd <= end) { + codePartStart = start + codePartEnd = codeEnd + } + // internal CDS part + else if (start < codeEnd && end > codeStart) { + codePartStart = start + codePartEnd = end + } + + // "splice in" the calculated cds part into subparts + // at beginning of _makeCDSs() method, bail if cds subparts are encountered + subparts.splice( + i, + 0, + new SimpleFeature({ + parent: parent, + uniqueId: parent.get('uniqueID') + ':CDS:' + i, + data: { + start: codePartStart, + end: codePartEnd, + strand: strand, + type: 'CDS', + name: parent.get('uniqueID') + ':CDS:' + i, + }, + }), + ) + } + + // make sure the subparts are sorted by coord + return subparts.sort((a, b) => a.get('start') - b.get('start')) + } + + protected makeUTRs(parent: Feature, subparts: Feature[]) { + // based on Lincoln's UTR-making code in + // Bio::Graphics::Glyph::processed_transcript + + let codeStart = Infinity + let codeEnd = -Infinity + + let haveLeftUTR + let haveRightUTR + + // gather exons, find coding start and end, and look for UTRs + let type + const exons = [] as Feature[] + subparts.forEach(sub => { + type = sub.get('type') + if (/^cds/i.test(type)) { + if (codeStart > sub.get('start')) { + codeStart = sub.get('start') + } + if (codeEnd < sub.get('end')) { + codeEnd = sub.get('end') + } + } else if (/exon/i.test(type)) { + exons.push(sub) + } else if (this.isUTR(sub)) { + haveLeftUTR = sub.get('start') === parent.get('start') + haveRightUTR = sub.get('end') === parent.get('end') + } + }) + + // bail if we don't have exons and CDS + if (!(exons.length && codeStart < Infinity && codeEnd > -Infinity)) { + return subparts + } + + // make sure the exons are sorted by coord + exons.sort((a, b) => a.get('start') - b.get('start')) + + const strand = parent.get('strand') + + // make the left-hand UTRs + let start + let end + if (!haveLeftUTR) { + for (let i = 0; i < exons.length; i++) { + start = exons[i].get('start') + if (start >= codeStart) { + break + } + end = codeStart > exons[i].get('end') ? exons[i].get('end') : codeStart + + subparts.unshift( + new SimpleFeature({ + parent: parent, + uniqueId: 'wow', // FIXME + data: { + start: start, + end: end, + strand: strand, + type: strand >= 0 ? 'five_prime_UTR' : 'three_prime_UTR', + }, + }), + ) + } + } + + // make the right-hand UTRs + if (!haveRightUTR) { + for (let i = exons.length - 1; i >= 0; i--) { + end = exons[i].get('end') + if (end <= codeEnd) { + break + } + + start = + codeEnd < exons[i].get('start') ? exons[i].get('start') : codeEnd + subparts.push( + new SimpleFeature({ + parent: parent, + uniqueId: 'wow', // FIXME + data: { + start: start, + end: end, + strand: strand, + type: strand >= 0 ? 'three_prime_UTR' : 'five_prime_UTR', + }, + }), + ) + } + } + + return subparts + } + + protected isUTR(feature: Feature) { + return /(\bUTR|_UTR|untranslated[_\s]region)\b/.test( + feature.get('type') || '', + ) + } + + getFeatureHeight(viewInfo: ViewInfo, feature: Feature) { + const height = super.getFeatureHeight(viewInfo, feature) + + if (this.isUTR(feature)) { + return height * 0.6 + } + + return height + } +} diff --git a/plugins/canvas/src/CanvasFeatureRenderer/FeatureGlyphs/Segments.ts b/plugins/canvas/src/CanvasFeatureRenderer/FeatureGlyphs/Segments.ts new file mode 100644 index 0000000000..131d48a2cd --- /dev/null +++ b/plugins/canvas/src/CanvasFeatureRenderer/FeatureGlyphs/Segments.ts @@ -0,0 +1,68 @@ +import { Feature } from '@jbrowse/core/util/simpleFeature' +import { bpSpanPx } from '@jbrowse/core/util' + +// locals +import { LaidOutFeatureRect, ViewInfo } from '../FeatureGlyph' +import BoxGlyph from './Box' + +export default class Segments extends BoxGlyph { + renderFeature( + context: CanvasRenderingContext2D, + viewInfo: ViewInfo, + fRect: LaidOutFeatureRect, + ) { + this.renderConnector(context, viewInfo, fRect) + this.renderSegments(context, viewInfo, fRect) + } + + renderConnector( + context: CanvasRenderingContext2D, + viewInfo: ViewInfo, + fRect: LaidOutFeatureRect, + ) { + const { bpPerPx, region } = viewInfo + const thickness = 1 + context.fillStyle = 'black' + const { + f, + t, + rect: { h }, + } = fRect + const [leftPx, rightPx] = bpSpanPx( + f.get('start'), + f.get('end'), + region, + bpPerPx, + ) + context.fillRect( + leftPx, + Math.round(t + (h - thickness) / 2), + rightPx - leftPx, + thickness, + ) + } + + renderSegments( + context: CanvasRenderingContext2D, + viewInfo: ViewInfo, + fRect: LaidOutFeatureRect, + ) { + const { t, f } = fRect + f.children()?.forEach(sub => { + this.renderSegment(context, viewInfo, sub, t, fRect.rect.h) + }) + } + + renderSegment( + context: CanvasRenderingContext2D, + viewInfo: ViewInfo, + feat: Feature, + topPx: number, + heightPx: number, + ) { + super.renderBox(context, viewInfo, feat, topPx, heightPx) + feat.children()?.forEach(sub => { + this.renderSegment(context, viewInfo, sub, topPx, heightPx) + }) + } +} diff --git a/plugins/canvas/src/CanvasFeatureRenderer/FeatureGlyphs/UnprocessedTranscript.ts b/plugins/canvas/src/CanvasFeatureRenderer/FeatureGlyphs/UnprocessedTranscript.ts new file mode 100644 index 0000000000..aa7959c1c3 --- /dev/null +++ b/plugins/canvas/src/CanvasFeatureRenderer/FeatureGlyphs/UnprocessedTranscript.ts @@ -0,0 +1,15 @@ +import { ViewInfo } from '../FeatureGlyph' +import { Feature } from '@jbrowse/core/util/simpleFeature' +import SegmentsGlyph from './Segments' + +export default class UnprocessedTranscript extends SegmentsGlyph { + renderBox( + context: CanvasRenderingContext2D, + viewInfo: ViewInfo, + feature: Feature, + top: number, + overallHeight: number, + ) { + return super.renderBox(context, viewInfo, feature, top, overallHeight) + } +} diff --git a/plugins/canvas/src/CanvasFeatureRenderer/components/CanvasFeatureRendering.js b/plugins/canvas/src/CanvasFeatureRenderer/components/CanvasFeatureRendering.js deleted file mode 100644 index 4faa321704..0000000000 --- a/plugins/canvas/src/CanvasFeatureRenderer/components/CanvasFeatureRendering.js +++ /dev/null @@ -1,8 +0,0 @@ -import React from 'react' -import { observer } from 'mobx-react' - -function CanvasFeatureRendering(props) { - return 'hello' -} - -export default observer(CanvasFeatureRendering) diff --git a/plugins/canvas/src/CanvasFeatureRenderer/components/CanvasFeatureRendering.tsx b/plugins/canvas/src/CanvasFeatureRenderer/components/CanvasFeatureRendering.tsx new file mode 100644 index 0000000000..f92e64b525 --- /dev/null +++ b/plugins/canvas/src/CanvasFeatureRenderer/components/CanvasFeatureRendering.tsx @@ -0,0 +1,255 @@ +import React, { useRef, useState, useEffect } from 'react' +import { Region } from '@jbrowse/core/util/types' +import { PrerenderedCanvas } from '@jbrowse/core/ui' +import { getContainingView } from '@jbrowse/core/util' +import { bpSpanPx } from '@jbrowse/core/util' +import { observer } from 'mobx-react' +import { isStateTreeNode } from 'mobx-state-tree' +import type { BaseLinearDisplayModel } from '@jbrowse/plugin-linear-genome-view' +import { postDraw } from '../CanvasFeatureRenderer' + +// used so that user can click-away-from-feature below the laid out features +// (issue #1248) +const canvasPadding = 100 + +function CanvasRendering(props: { + blockKey: string + displayModel: BaseLinearDisplayModel + width: number + height: number + regions: Region[] + bpPerPx: number + layoutRecords: { + start: number + end: number + t: number + l: number + description: { text: string; yOffset: number } + label: { text: string; yOffset: number } + }[] + onMouseMove?: (event: React.MouseEvent, featureId: string | undefined) => void +}) { + const { + onMouseMove, + blockKey, + displayModel, + width, + height, + regions, + bpPerPx, + layoutRecords, + } = props + + const { selectedFeatureId, featureIdUnderMouse, contextMenuFeature } = + displayModel + const view = isStateTreeNode(displayModel) + ? getContainingView(displayModel) + : undefined + + // @ts-ignore + const { dynamicBlocks, staticBlocks, offsetPx: viewOffsetPx } = view || {} + const { offsetPx: blockOffsetPx } = staticBlocks?.contentBlocks[0] || {} + const { start: viewStart } = dynamicBlocks?.contentBlocks[0] || {} + const offsetPx = viewOffsetPx - blockOffsetPx + + const [region] = regions + const highlightOverlayCanvas = useRef(null) + const labelsCanvas = useRef(null) + const [mouseIsDown, setMouseIsDown] = useState(false) + const [movedDuringLastMouseDown, setMovedDuringLastMouseDown] = + useState(false) + + useEffect(() => { + const canvas = labelsCanvas.current + if (!canvas) { + return + } + const ctx = canvas.getContext('2d') + if (!ctx) { + return + } + ctx.clearRect(0, 0, canvas.width, canvas.height) + postDraw({ + ctx, + layoutRecords, + offsetPx, + regions: [{ start: viewStart }], + }) + }, [layoutRecords, viewStart, offsetPx]) + useEffect(() => { + const canvas = highlightOverlayCanvas.current + if (!canvas) { + return + } + const ctx = canvas.getContext('2d') + if (!ctx) { + return + } + ctx.clearRect(0, 0, canvas.width, canvas.height) + const selectedRect = selectedFeatureId + ? displayModel.getFeatureByID?.(blockKey, selectedFeatureId) + : undefined + if (selectedRect) { + const [leftBp, topPx, rightBp, bottomPx] = selectedRect + const [leftPx, rightPx] = bpSpanPx(leftBp, rightBp, region, bpPerPx) + const rectTop = Math.round(topPx) + const rectHeight = Math.round(bottomPx - topPx) + ctx.shadowColor = '#222266' + ctx.shadowBlur = 10 + ctx.lineJoin = 'bevel' + ctx.lineWidth = 2 + ctx.strokeStyle = '#00b8ff' + ctx.strokeRect( + leftPx - 2, + rectTop - 2, + rightPx - leftPx + 4, + rectHeight + 4, + ) + ctx.clearRect(leftPx, rectTop, rightPx - leftPx, rectHeight) + } + const highlightedFeature = featureIdUnderMouse || contextMenuFeature?.id() + const highlightedRect = highlightedFeature + ? displayModel.getFeatureByID?.(blockKey, highlightedFeature) + : undefined + if (highlightedRect) { + const [leftBp, topPx, rightBp, bottomPx] = highlightedRect + const [leftPx, rightPx] = bpSpanPx(leftBp, rightBp, region, bpPerPx) + const rectTop = Math.round(topPx) + const rectHeight = Math.round(bottomPx - topPx) + ctx.fillStyle = '#0003' + ctx.fillRect(leftPx, rectTop, rightPx - leftPx, rectHeight) + } + }, [ + bpPerPx, + region, + blockKey, + selectedFeatureId, + displayModel, + featureIdUnderMouse, + contextMenuFeature, + ]) + + function onMouseDown(event: React.MouseEvent) { + setMouseIsDown(true) + setMovedDuringLastMouseDown(false) + callMouseHandler('MouseDown', event) + } + + function onMouseEnter(event: React.MouseEvent) { + callMouseHandler('MouseEnter', event) + } + + function onMouseOut(event: React.MouseEvent) { + callMouseHandler('MouseOut', event) + callMouseHandler('MouseLeave', event) + } + + function onMouseOver(event: React.MouseEvent) { + callMouseHandler('MouseOver', event) + } + + function onMouseUp(event: React.MouseEvent) { + setMouseIsDown(false) + callMouseHandler('MouseUp', event) + } + + function onClick(event: React.MouseEvent) { + if (!movedDuringLastMouseDown) { + callMouseHandler('Click', event) + } + } + + function onMouseLeave(event: React.MouseEvent) { + callMouseHandler('MouseOut', event) + callMouseHandler('MouseLeave', event) + } + + function onContextMenu(event: React.MouseEvent) { + callMouseHandler('ContextMenu', event) + } + + function mouseMove(event: React.MouseEvent) { + if (mouseIsDown) { + setMovedDuringLastMouseDown(true) + } + let offsetX = 0 + let offsetY = 0 + const canvas = highlightOverlayCanvas.current + if (canvas) { + const { left, top } = canvas.getBoundingClientRect() + offsetX = left + offsetY = top + } + offsetX = event.clientX - offsetX + offsetY = event.clientY - offsetY + const px = region.reversed ? width - offsetX : offsetX + const clientBp = region.start + bpPerPx * px + + const featIdUnderMouse = displayModel.getFeatureOverlapping( + blockKey, + clientBp, + offsetY, + ) + + if (onMouseMove) { + onMouseMove(event, featIdUnderMouse) + } + } + + function callMouseHandler(handlerName: string, event: React.MouseEvent) { + // @ts-ignore + const featureHandler = props[`onFeature${handlerName}`] + // @ts-ignore + const canvasHandler = props[`on${handlerName}`] + if (featureHandler && featureIdUnderMouse) { + featureHandler(event, featureIdUnderMouse) + } else if (canvasHandler) { + canvasHandler(event, featureIdUnderMouse) + } + } + + const canvasWidth = Math.ceil(width) + return ( +
+ + onMouseDown(event)} + onMouseEnter={event => onMouseEnter(event)} + onMouseOut={event => onMouseOut(event)} + onMouseOver={event => onMouseOver(event)} + onMouseUp={event => onMouseUp(event)} + onMouseLeave={event => onMouseLeave(event)} + onMouseMove={event => mouseMove(event)} + onClick={event => onClick(event)} + onContextMenu={event => onContextMenu(event)} + onFocus={() => {}} + onBlur={() => {}} + /> + +
+ ) +} + +export default observer(CanvasRendering) diff --git a/plugins/canvas/src/CanvasFeatureRenderer/index.ts b/plugins/canvas/src/CanvasFeatureRenderer/index.ts index 62e37a7ab1..7d97a45f18 100644 --- a/plugins/canvas/src/CanvasFeatureRenderer/index.ts +++ b/plugins/canvas/src/CanvasFeatureRenderer/index.ts @@ -1,2 +1,5 @@ +import CanvasFeatureRenderer from './CanvasFeatureRenderer' + export { default as ReactComponent } from './components/CanvasFeatureRendering' export { default as configSchema } from './configSchema' +export default CanvasFeatureRenderer diff --git a/plugins/canvas/src/index.ts b/plugins/canvas/src/index.ts index be6142d1a0..52f9d15408 100644 --- a/plugins/canvas/src/index.ts +++ b/plugins/canvas/src/index.ts @@ -1,15 +1,10 @@ -import BoxRendererType from '@jbrowse/core/pluggableElementTypes/renderers/BoxRendererType' import Plugin from '@jbrowse/core/Plugin' import PluginManager from '@jbrowse/core/PluginManager' -import { +import CanvasFeatureRenderer, { configSchema as canvasFeatureRendererConfigSchema, ReactComponent as CanvasFeatureRendererReactComponent, } from './CanvasFeatureRenderer' -class CanvasFeatureRenderer extends BoxRendererType { - supportsSVG = true -} - export default class CanvasPlugin extends Plugin { name = 'CanvasPlugin' diff --git a/plugins/linear-genome-view/src/LinearGenomeView/components/ExportSvgDialog.tsx b/plugins/linear-genome-view/src/LinearGenomeView/components/ExportSvgDialog.tsx index 5ea7c16f48..c40d4adf73 100644 --- a/plugins/linear-genome-view/src/LinearGenomeView/components/ExportSvgDialog.tsx +++ b/plugins/linear-genome-view/src/LinearGenomeView/components/ExportSvgDialog.tsx @@ -1,5 +1,4 @@ import React, { useState } from 'react' -import { makeStyles } from '@material-ui/core/styles' import { Button, Dialog, @@ -11,6 +10,7 @@ import { FormControlLabel, CircularProgress, Typography, + makeStyles, } from '@material-ui/core' import CloseIcon from '@material-ui/icons/Close' import { LinearGenomeViewModel as LGV } from '..' diff --git a/plugins/svg/src/SvgFeatureRenderer/components/SvgFeatureRendering.js b/plugins/svg/src/SvgFeatureRenderer/components/SvgFeatureRendering.js index 6478f68ee9..25c50e70e4 100644 --- a/plugins/svg/src/SvgFeatureRenderer/components/SvgFeatureRendering.js +++ b/plugins/svg/src/SvgFeatureRenderer/components/SvgFeatureRendering.js @@ -6,9 +6,13 @@ import { observer } from 'mobx-react' import ReactPropTypes from 'prop-types' import React, { useEffect, useRef, useState, useCallback } from 'react' import FeatureGlyph from './FeatureGlyph' -import { OverlayRect } from './SvgOverlay' +import SvgOverlay from './SvgOverlay' import { chooseGlyphComponent, layOut } from './util' +const renderingStyle = { + position: 'relative', +} + // used to make features have a little padding for their labels const nameWidthPadding = 2 const textVerticalPadding = 2 @@ -171,30 +175,26 @@ function SvgFeatureRendering(props) { config, displayModel, exportSVG, - onMouseOut, - onMouseDown, - onMouseLeave, - onMouseEnter, - onMouseOver, - onMouseMove, - onMouseUp, - onClick, - ...handlers } = props - const { selectedFeatureId, featureIdUnderMouse, contextMenuFeature } = - displayModel const [region] = regions || [] const width = (region.end - region.start) / bpPerPx const displayMode = readConfObject(config, 'displayMode') - const [highlightRect, setHighlightRect] = useState() - const [mouseoverRect, setMouseoverRect] = useState() const ref = useRef() const [mouseIsDown, setMouseIsDown] = useState(false) const [movedDuringLastMouseDown, setMovedDuringLastMouseDown] = useState(false) const [height, setHeight] = useState(0) - const mouseoverFeatureId = featureIdUnderMouse || contextMenuFeature?.id() + const { + onMouseOut, + onMouseDown, + onMouseLeave, + onMouseEnter, + onMouseOver, + onMouseMove, + onMouseUp, + onClick, + } = props const mouseDown = useCallback( event => { @@ -272,12 +272,9 @@ function SvgFeatureRendering(props) { } let offsetX = 0 let offsetY = 0 - - const canvas = ref.current - if (canvas) { - const { left, top } = canvas.getBoundingClientRect() - offsetX = left - offsetY = top + if (ref.current) { + offsetX = ref.current.getBoundingClientRect().left + offsetY = ref.current.getBoundingClientRect().top } offsetX = event.clientX - offsetX offsetY = event.clientY - offsetY @@ -319,117 +316,10 @@ function SvgFeatureRendering(props) { [movedDuringLastMouseDown, onClick], ) - function onFeatureMouseDown( - event: React.MouseEvent, - ) { - const { onFeatureMouseDown: handler } = handlers - if (!(handler && mouseoverFeatureId)) { - return undefined - } - return handler(event, mouseoverFeatureId) - } - - function onFeatureMouseEnter( - event: React.MouseEvent, - ) { - const { onFeatureMouseEnter: handler } = handlers - if (!(handler && mouseoverFeatureId)) { - return undefined - } - return handler(event, mouseoverFeatureId) - } - - function onFeatureMouseOut( - event: - | React.MouseEvent - | React.FocusEvent, - ) { - const { onFeatureMouseOut: handler } = handlers - if (!(handler && mouseoverFeatureId)) { - return undefined - } - return handler(event, mouseoverFeatureId) - } - - function onFeatureMouseOver( - event: - | React.MouseEvent - | React.FocusEvent, - ) { - const { onFeatureMouseOver: handler } = handlers - if (!(handler && mouseoverFeatureId)) { - return undefined - } - return handler(event, mouseoverFeatureId) - } - - function onFeatureMouseUp( - event: React.MouseEvent, - ) { - const { onFeatureMouseUp: handler } = handlers - if (!(handler && mouseoverFeatureId)) { - return undefined - } - return handler(event, mouseoverFeatureId) - } - - function onFeatureMouseLeave( - event: React.MouseEvent, - ) { - const { onFeatureMouseLeave: handler } = handlers - if (!(handler && mouseoverFeatureId)) { - return undefined - } - return handler(event, mouseoverFeatureId) - } - - function onFeatureMouseMove( - event: React.MouseEvent, - ) { - const { onFeatureMouseMove: handler } = handlers - if (!(handler && mouseoverFeatureId)) { - return undefined - } - return handler(event, mouseoverFeatureId) - } - - function onFeatureClick(event: React.MouseEvent) { - if (movedDuringLastMouseDown) { - return undefined - } - const { onFeatureClick: handler } = handlers - if (!(handler && mouseoverFeatureId)) { - return undefined - } - event.stopPropagation() - return handler(event, mouseoverFeatureId) - } - - function onFeatureContextMenu( - event: React.MouseEvent, - ) { - const { onFeatureContextMenu: handler } = handlers - if (!(handler && mouseoverFeatureId)) { - return undefined - } - return handler(event, mouseoverFeatureId) - } - useEffect(() => { setHeight(layout.getTotalHeight()) }, [layout]) - useEffect(() => { - setMouseoverRect( - displayModel.getFeatureByID?.(blockKey, featureIdUnderMouse), - ) - }, [blockKey, displayModel, featureIdUnderMouse]) - - useEffect(() => { - setHighlightRect(displayModel.getFeatureByID?.(blockKey, selectedFeatureId)) - }, [blockKey, displayModel, selectedFeatureId]) - - if (exportSVG) { return ( +
- {mouseoverRect ? ( - - ) : null} - {highlightRect ? ( - - ) : null} +
) @@ -518,7 +379,6 @@ SvgFeatureRendering.propTypes = { configuration: ReactPropTypes.shape({}), getFeatureOverlapping: ReactPropTypes.func, selectedFeatureId: ReactPropTypes.string, - contextMenuFeature: ReactPropTypes.shape({ id: ReactPropTypes.func }), featureIdUnderMouse: ReactPropTypes.string, }), diff --git a/plugins/svg/src/SvgFeatureRenderer/components/SvgOverlay.tsx b/plugins/svg/src/SvgFeatureRenderer/components/SvgOverlay.tsx index fbbc616b47..469d2f7ecf 100644 --- a/plugins/svg/src/SvgFeatureRenderer/components/SvgOverlay.tsx +++ b/plugins/svg/src/SvgFeatureRenderer/components/SvgOverlay.tsx @@ -1,8 +1,63 @@ import { bpSpanPx } from '@jbrowse/core/util' +import SimpleFeature from '@jbrowse/core/util/simpleFeature' import { Region } from '@jbrowse/core/util/types' +import { observer } from 'mobx-react' import React from 'react' type LayoutRecord = [number, number, number, number] +interface SvgOverlayProps { + region: Region + displayModel: { + getFeatureByID: (arg0: string, arg1: string) => LayoutRecord + selectedFeatureId?: string + featureIdUnderMouse?: string + contextMenuFeature?: SimpleFeature + } + bpPerPx: number + blockKey: string + movedDuringLastMouseDown: boolean + onFeatureMouseDown?( + event: React.MouseEvent, + featureId: string, + ): {} + onFeatureMouseEnter?( + event: React.MouseEvent, + featureId: string, + ): {} + onFeatureMouseOut?( + event: + | React.MouseEvent + | React.FocusEvent, + featureId: string, + ): {} + onFeatureMouseOver?( + event: + | React.MouseEvent + | React.FocusEvent, + featureId: string, + ): {} + onFeatureMouseUp?( + event: React.MouseEvent, + featureId: string, + ): {} + onFeatureMouseLeave?( + event: React.MouseEvent, + featureId: string, + ): {} + onFeatureMouseMove?( + event: React.MouseEvent, + featureId: string, + ): {} + // synthesized from mouseup and mousedown + onFeatureClick?( + event: React.MouseEvent, + featureId: string, + ): {} + onFeatureContextMenu?( + event: React.MouseEvent, + featureId: string, + ): {} +} interface OverlayRectProps extends React.SVGProps { rect?: LayoutRecord @@ -10,7 +65,7 @@ interface OverlayRectProps extends React.SVGProps { bpPerPx: number } -export function OverlayRect({ +function OverlayRect({ rect, region, bpPerPx, @@ -43,3 +98,150 @@ export function OverlayRect({ /> ) } + +function SvgOverlay({ + displayModel, + blockKey, + region, + bpPerPx, + movedDuringLastMouseDown, + ...handlers +}: SvgOverlayProps) { + const { selectedFeatureId, featureIdUnderMouse, contextMenuFeature } = + displayModel + + const mouseoverFeatureId = featureIdUnderMouse || contextMenuFeature?.id() + + function onFeatureMouseDown( + event: React.MouseEvent, + ) { + const { onFeatureMouseDown: handler } = handlers + if (!(handler && mouseoverFeatureId)) { + return undefined + } + return handler(event, mouseoverFeatureId) + } + + function onFeatureMouseEnter( + event: React.MouseEvent, + ) { + const { onFeatureMouseEnter: handler } = handlers + if (!(handler && mouseoverFeatureId)) { + return undefined + } + return handler(event, mouseoverFeatureId) + } + + function onFeatureMouseOut( + event: + | React.MouseEvent + | React.FocusEvent, + ) { + const { onFeatureMouseOut: handler } = handlers + if (!(handler && mouseoverFeatureId)) { + return undefined + } + return handler(event, mouseoverFeatureId) + } + + function onFeatureMouseOver( + event: + | React.MouseEvent + | React.FocusEvent, + ) { + const { onFeatureMouseOver: handler } = handlers + if (!(handler && mouseoverFeatureId)) { + return undefined + } + return handler(event, mouseoverFeatureId) + } + + function onFeatureMouseUp( + event: React.MouseEvent, + ) { + const { onFeatureMouseUp: handler } = handlers + if (!(handler && mouseoverFeatureId)) { + return undefined + } + return handler(event, mouseoverFeatureId) + } + + function onFeatureMouseLeave( + event: React.MouseEvent, + ) { + const { onFeatureMouseLeave: handler } = handlers + if (!(handler && mouseoverFeatureId)) { + return undefined + } + return handler(event, mouseoverFeatureId) + } + + function onFeatureMouseMove( + event: React.MouseEvent, + ) { + const { onFeatureMouseMove: handler } = handlers + if (!(handler && mouseoverFeatureId)) { + return undefined + } + return handler(event, mouseoverFeatureId) + } + + function onFeatureClick(event: React.MouseEvent) { + if (movedDuringLastMouseDown) { + return undefined + } + const { onFeatureClick: handler } = handlers + if (!(handler && mouseoverFeatureId)) { + return undefined + } + event.stopPropagation() + return handler(event, mouseoverFeatureId) + } + + function onFeatureContextMenu( + event: React.MouseEvent, + ) { + const { onFeatureContextMenu: handler } = handlers + if (!(handler && mouseoverFeatureId)) { + return undefined + } + return handler(event, mouseoverFeatureId) + } + + return ( + <> + {mouseoverFeatureId ? ( + + ) : null} + {selectedFeatureId ? ( + + ) : null} + + ) +} + +export default observer(SvgOverlay) diff --git a/test_data/config_demo.json b/test_data/config_demo.json index d749dbf564..55700f289f 100644 --- a/test_data/config_demo.json +++ b/test_data/config_demo.json @@ -165,7 +165,7 @@ "type": "LinearBasicDisplay", "displayId": "nclist_genes_hg19_linear", "renderer": { - "type": "SvgFeatureRenderer", + "type": "CanvasFeatureRenderer", "labels": { "description": "jexl:get(feature,'gene_name')" } @@ -275,7 +275,7 @@ "labels": { "name": "jexl:get(feature,'genomicDnaChange')" }, - "type": "SvgFeatureRenderer" + "type": "CanvasFeatureRenderer" } } ] @@ -394,7 +394,7 @@ "type": "LinearBasicDisplay", "displayId": "gencode_nclist_hg38_linear", "renderer": { - "type": "SvgFeatureRenderer", + "type": "CanvasFeatureRenderer", "labels": { "description": "jexl:get(feature,'gene_name')" } @@ -448,7 +448,7 @@ "type": "LinearBasicDisplay", "displayId": "mane_hg38_linear", "renderer": { - "type": "SvgFeatureRenderer", + "type": "CanvasFeatureRenderer", "labels": { "description": "jexl:get(feature,'geneName2')" } @@ -2716,7 +2716,7 @@ "type": "LinearBasicDisplay", "displayId": "gencode_nclist_v36_hg37_lift_linear", "renderer": { - "type": "SvgFeatureRenderer", + "type": "CanvasFeatureRenderer", "labels": { "description": "jexl:get(feature,'gene_name')" } @@ -2749,7 +2749,7 @@ "type": "LinearBasicDisplay", "displayId": "gencode_nclist_v36_hg38_linear", "renderer": { - "type": "SvgFeatureRenderer", + "type": "CanvasFeatureRenderer", "labels": { "description": "jexl:get(feature,'gene_name')" } @@ -3394,7 +3394,7 @@ "type": "LinearBasicDisplay", "displayId": "testing1", "renderer": { - "type": "SvgFeatureRenderer", + "type": "CanvasFeatureRenderer", "labels": { "description": "jexl:get(feature,'ucscLabel')" } From 1ab3246d00c82c309d5e4b2e66a1dfb0bf3cd43d Mon Sep 17 00:00:00 2001 From: Colin Date: Sun, 24 Oct 2021 16:54:41 -0400 Subject: [PATCH 07/28] Remove padding conditional --- plugins/canvas/src/CanvasFeatureRenderer/FeatureGlyphs/Box.ts | 2 +- plugins/canvas/src/CanvasFeatureRenderer/FeatureGlyphs/Gene.ts | 3 +-- .../canvas/src/CanvasFeatureRenderer/FeatureGlyphs/Segments.ts | 2 +- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/plugins/canvas/src/CanvasFeatureRenderer/FeatureGlyphs/Box.ts b/plugins/canvas/src/CanvasFeatureRenderer/FeatureGlyphs/Box.ts index 5f620241e9..a9069a06b5 100644 --- a/plugins/canvas/src/CanvasFeatureRenderer/FeatureGlyphs/Box.ts +++ b/plugins/canvas/src/CanvasFeatureRenderer/FeatureGlyphs/Box.ts @@ -156,7 +156,7 @@ export default class Box extends FeatureGlyph { const height = this.getFeatureHeight(viewInfo, feature) if (height !== overallHeight) { - top += Math.round((overallHeight - height) / 2) + top += (overallHeight - height) / 2 } context.fillStyle = readConfObject(config, 'color1', { feature }) diff --git a/plugins/canvas/src/CanvasFeatureRenderer/FeatureGlyphs/Gene.ts b/plugins/canvas/src/CanvasFeatureRenderer/FeatureGlyphs/Gene.ts index 9f3dde4228..0f39a20e69 100644 --- a/plugins/canvas/src/CanvasFeatureRenderer/FeatureGlyphs/Gene.ts +++ b/plugins/canvas/src/CanvasFeatureRenderer/FeatureGlyphs/Gene.ts @@ -28,7 +28,6 @@ export default class Gene extends BoxGlyph { } // get the rects for the children - let padding = 1 const fRect = { l: 0, h: 0, @@ -49,7 +48,7 @@ export default class Gene extends BoxGlyph { const glyph = this.getSubGlyph(sub) const subRect = glyph.getFeatureRectangle(subArgs, sub) - padding = i === subfeatures.length - 1 ? 0 : 1 + const padding = 1 const newTop = fRect.h ? fRect.h + padding : 0 subRect.t = newTop subRect.rect.t = newTop diff --git a/plugins/canvas/src/CanvasFeatureRenderer/FeatureGlyphs/Segments.ts b/plugins/canvas/src/CanvasFeatureRenderer/FeatureGlyphs/Segments.ts index 131d48a2cd..fa42ef0f08 100644 --- a/plugins/canvas/src/CanvasFeatureRenderer/FeatureGlyphs/Segments.ts +++ b/plugins/canvas/src/CanvasFeatureRenderer/FeatureGlyphs/Segments.ts @@ -36,7 +36,7 @@ export default class Segments extends BoxGlyph { ) context.fillRect( leftPx, - Math.round(t + (h - thickness) / 2), + t + (h - thickness) / 2, rightPx - leftPx, thickness, ) From 9ce15adc60579523b0af0e1f8c7edd31bda9faf8 Mon Sep 17 00:00:00 2001 From: Colin Date: Mon, 25 Oct 2021 09:23:28 -0400 Subject: [PATCH 08/28] Misc --- plugins/canvas/src/CanvasFeatureRenderer/FeatureGlyph.ts | 9 +++++++-- .../src/CanvasFeatureRenderer/FeatureGlyphs/Gene.ts | 9 +++++++-- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/plugins/canvas/src/CanvasFeatureRenderer/FeatureGlyph.ts b/plugins/canvas/src/CanvasFeatureRenderer/FeatureGlyph.ts index 6367da101c..125efa31b4 100644 --- a/plugins/canvas/src/CanvasFeatureRenderer/FeatureGlyph.ts +++ b/plugins/canvas/src/CanvasFeatureRenderer/FeatureGlyph.ts @@ -3,6 +3,7 @@ import { Feature } from '@jbrowse/core/util/simpleFeature' import { AnyConfigurationModel } from '@jbrowse/core/configuration/configurationSchema' import { bpSpanPx } from '@jbrowse/core/util' import { Region } from '@jbrowse/core/util/types' +import { BaseLayout } from '@jbrowse/core/util/layouts/BaseLayout' export interface ViewInfo { bpPerPx: number @@ -22,7 +23,11 @@ export interface LaidOutFeatureRect extends FeatureRect { } export default class FeatureGlyph { - layoutFeature(viewArgs: ViewInfo, layout: any, feature: Feature) { + layoutFeature( + viewArgs: ViewInfo, + layout: BaseLayout, + feature: Feature, + ) { const fRect = this.getFeatureRectangle(viewArgs, feature) const { region, bpPerPx } = viewArgs @@ -30,7 +35,7 @@ export default class FeatureGlyph { const leftBase = region.start const startbp = fRect.l / scale + leftBase const endbp = (fRect.l + fRect.w) / scale + leftBase - const top = layout.addRect(feature.id(), startbp, endbp, fRect.h, feature) + const top = layout.addRect(feature.id(), startbp, endbp, fRect.h) if (top === null) { return null } diff --git a/plugins/canvas/src/CanvasFeatureRenderer/FeatureGlyphs/Gene.ts b/plugins/canvas/src/CanvasFeatureRenderer/FeatureGlyphs/Gene.ts index 0f39a20e69..9628e71845 100644 --- a/plugins/canvas/src/CanvasFeatureRenderer/FeatureGlyphs/Gene.ts +++ b/plugins/canvas/src/CanvasFeatureRenderer/FeatureGlyphs/Gene.ts @@ -1,4 +1,5 @@ import { Feature } from '@jbrowse/core/util/simpleFeature' +import { BaseLayout } from '@jbrowse/core/util/layouts/BaseLayout' // locals import BoxGlyph from './Box' @@ -44,7 +45,7 @@ export default class Gene extends BoxGlyph { fRect.l = Infinity fRect.r = -Infinity - subfeatures.forEach((sub, i) => { + subfeatures.forEach(sub => { const glyph = this.getSubGlyph(sub) const subRect = glyph.getFeatureRectangle(subArgs, sub) @@ -81,7 +82,11 @@ export default class Gene extends BoxGlyph { return this.expandRectangleWithLabels(viewArgs, feature, fRect) } - layoutFeature(viewInfo: ViewInfo, layout: any, feature: Feature) { + layoutFeature( + viewInfo: ViewInfo, + layout: BaseLayout, + feature: Feature, + ) { const fRect = super.layoutFeature( viewInfo, layout, From f8f6dfbdefb301fdd32908a2ebae03a381fff2e3 Mon Sep 17 00:00:00 2001 From: Colin Date: Mon, 25 Oct 2021 14:17:02 -0400 Subject: [PATCH 09/28] Move files around --- ...eRenderer.tsx => CanvasFeatureRenderer.ts} | 108 ++++++------------ .../FeatureGlyphs/Box.ts | 24 ++++ .../components/CanvasFeatureRendering.tsx | 14 ++- 3 files changed, 74 insertions(+), 72 deletions(-) rename plugins/canvas/src/CanvasFeatureRenderer/{CanvasFeatureRenderer.tsx => CanvasFeatureRenderer.ts} (64%) diff --git a/plugins/canvas/src/CanvasFeatureRenderer/CanvasFeatureRenderer.tsx b/plugins/canvas/src/CanvasFeatureRenderer/CanvasFeatureRenderer.ts similarity index 64% rename from plugins/canvas/src/CanvasFeatureRenderer/CanvasFeatureRenderer.tsx rename to plugins/canvas/src/CanvasFeatureRenderer/CanvasFeatureRenderer.ts index 69a043396c..f2247890a4 100644 --- a/plugins/canvas/src/CanvasFeatureRenderer/CanvasFeatureRenderer.tsx +++ b/plugins/canvas/src/CanvasFeatureRenderer/CanvasFeatureRenderer.ts @@ -8,12 +8,18 @@ import BoxRendererType, { } from '@jbrowse/core/pluggableElementTypes/renderers/BoxRendererType' import { Feature } from '@jbrowse/core/util/simpleFeature' import { BaseLayout } from '@jbrowse/core/util/layouts/BaseLayout' -import { iterMap, bpSpanPx } from '@jbrowse/core/util' +import { iterMap } from '@jbrowse/core/util' import { renderToAbstractCanvas } from '@jbrowse/core/util/offscreenCanvasUtils' + +// locals import BoxGlyph from './FeatureGlyphs/Box' import GeneGlyph from './FeatureGlyphs/Gene' import { LaidOutFeatureRect } from './FeatureGlyph' +export interface LaidOutFeatureRectWithGlyph extends LaidOutFeatureRect { + glyph: BoxGlyph +} + export type { RenderArgs, RenderArgsSerialized, @@ -42,55 +48,38 @@ export default class CanvasRenderer extends BoxRendererType { feature: Feature, layout: BaseLayout, props: RenderArgsDeserialized, - ): LaidOutFeatureRect | null { - const glyph = new BoxGlyph() - const geneglyph = new GeneGlyph() + ): LaidOutFeatureRectWithGlyph | null { const [region] = props.regions + let glyph if (feature.get('type') === 'gene') { - return geneglyph.layoutFeature({ region, ...props }, layout, feature) + glyph = new GeneGlyph() } else { - return glyph.layoutFeature({ region, ...props }, layout, feature) + glyph = new BoxGlyph() } + const fRect = glyph.layoutFeature({ region, ...props }, layout, feature) + return fRect ? { ...fRect, glyph } : null } drawRect( ctx: CanvasRenderingContext2D, - fRect: LaidOutFeatureRect, + fRect: LaidOutFeatureRectWithGlyph, props: RenderArgsDeserialized, ) { const { regions } = props const [region] = regions - - const glyph = new BoxGlyph() - const geneglyph = new GeneGlyph() - - if (fRect.f.get('type') === 'gene') { - geneglyph.renderFeature(ctx, { ...props, region }, fRect) - } else { - glyph.renderFeature(ctx, { ...props, region }, fRect) - } + fRect.glyph.renderFeature(ctx, { ...props, region }, fRect) } async makeImageData( ctx: CanvasRenderingContext2D, - layoutRecords: LaidOutFeatureRect[], + layoutRecords: LaidOutFeatureRectWithGlyph[], props: RenderArgsDeserializedWithFeaturesAndLayout, ) { - layoutRecords - .filter(f => !!f) - .forEach(fRect => { - this.drawRect(ctx, fRect, props) - }) + layoutRecords.forEach(fRect => { + this.drawRect(ctx, fRect, props) + }) if (props.exportSVG) { - // console.log( - // bpSpanPx( - // props.regions[0].start, - // props.regions[0].start + 1, - // props.regions[0], - // props.bpPerPx, - // )[0], - // ) postDraw({ ctx, layoutRecords: layoutRecords.map(f => ({ @@ -113,11 +102,13 @@ export default class CanvasRenderer extends BoxRendererType { const layout = this.createLayoutInWorker(renderProps) const [region] = regions const featureMap = features + const layoutRecords = iterMap( featureMap.values(), feature => this.layoutFeature(feature, layout, renderProps), featureMap.size, - ).filter((f): f is LaidOutFeatureRect => !!f) + ).filter((f): f is LaidOutFeatureRectWithGlyph => !!f) + const width = (region.end - region.start) / bpPerPx const height = Math.max(layout.getTotalHeight(), 1) @@ -144,13 +135,14 @@ export default class CanvasRenderer extends BoxRendererType { return { ...results, ...res, - layoutRecords: layoutRecords.map(f => ({ - label: f.label, - description: f.description, - l: f.l, - t: f.t, - start: f.f.get('start'), - end: f.f.get('end'), + layoutRecords: layoutRecords.map(({ f, l, t, label, description }) => ({ + label, + description, + l, + t, + type: f.get('type'), + start: f.get('start'), + end: f.get('end'), })), features, layout, @@ -170,45 +162,19 @@ export function postDraw({ ctx: CanvasRenderingContext2D regions: { start: number }[] offsetPx: number - layoutRecords: any + layoutRecords: LaidOutFeatureRectWithGlyph[] }) { const [region] = regions ctx.fillStyle = 'black' - ctx.font = '9px sans-serif' + ctx.font = '10px sans-serif' layoutRecords .filter(f => !!f) .forEach(record => { - const { - start, - end, - l, - t, - label: { text, yOffset }, - } = record - - if (start < region.start && region.start < end) { - ctx.fillText(text, offsetPx, t + yOffset) - } else { - ctx.fillText(text, l, t + yOffset) - } - }) - - ctx.fillStyle = 'blue' - layoutRecords - .filter(f => !!f) - .forEach(record => { - const { - start, - end, - l, - t, - description: { text, yOffset }, - } = record - if (start < region.start && region.start < end) { - ctx.fillText(text, offsetPx, t + yOffset) - } else { - ctx.fillText(text, l, t + yOffset) - } + record.glyph.postDraw(ctx, { + record, + regionStart: region.start, + offsetPx, + }) }) } diff --git a/plugins/canvas/src/CanvasFeatureRenderer/FeatureGlyphs/Box.ts b/plugins/canvas/src/CanvasFeatureRenderer/FeatureGlyphs/Box.ts index a9069a06b5..bfc0294537 100644 --- a/plugins/canvas/src/CanvasFeatureRenderer/FeatureGlyphs/Box.ts +++ b/plugins/canvas/src/CanvasFeatureRenderer/FeatureGlyphs/Box.ts @@ -162,4 +162,28 @@ export default class Box extends FeatureGlyph { context.fillStyle = readConfObject(config, 'color1', { feature }) context.fillRect(left, top, Math.max(1, width), height) } + + postDraw( + ctx: CanvasRenderingContext2D, + props: { + record: LaidOutFeatureRect + regionStart: number + offsetPx: number + }, + ) { + const { regionStart, record, offsetPx } = props + const { + start, + end, + l, + t, + label: { text, yOffset }, + } = record + + if (start < regionStart && regionStart < end) { + ctx.fillText(text, offsetPx, t + yOffset) + } else { + ctx.fillText(text, l, t + yOffset) + } + } } diff --git a/plugins/canvas/src/CanvasFeatureRenderer/components/CanvasFeatureRendering.tsx b/plugins/canvas/src/CanvasFeatureRenderer/components/CanvasFeatureRendering.tsx index f92e64b525..a2fb8edafa 100644 --- a/plugins/canvas/src/CanvasFeatureRenderer/components/CanvasFeatureRendering.tsx +++ b/plugins/canvas/src/CanvasFeatureRenderer/components/CanvasFeatureRendering.tsx @@ -8,6 +8,10 @@ import { isStateTreeNode } from 'mobx-state-tree' import type { BaseLinearDisplayModel } from '@jbrowse/plugin-linear-genome-view' import { postDraw } from '../CanvasFeatureRenderer' +// locals +import BoxGlyph from '../FeatureGlyphs/Box' +import GeneGlyph from '../FeatureGlyphs/Gene' + // used so that user can click-away-from-feature below the laid out features // (issue #1248) const canvasPadding = 100 @@ -71,7 +75,15 @@ function CanvasRendering(props: { ctx.clearRect(0, 0, canvas.width, canvas.height) postDraw({ ctx, - layoutRecords, + layoutRecords: layoutRecords.map(f => { + let glyph + if (f.type === 'gene') { + glyph = new GeneGlyph() + } else { + glyph = new BoxGlyph() + } + return { ...f, glyph } + }), offsetPx, regions: [{ start: viewStart }], }) From a22ad3b08c120f3ba7df0dfc782aa510153eabf9 Mon Sep 17 00:00:00 2001 From: Colin Date: Mon, 25 Oct 2021 14:28:31 -0400 Subject: [PATCH 10/28] Incremental --- .../src/CanvasFeatureRenderer/FeatureGlyph.ts | 2 + .../FeatureGlyphs/Box.ts | 39 +++++++++++-------- .../FeatureGlyphs/Gene.ts | 5 ++- .../components/CanvasFeatureRendering.tsx | 10 +---- 4 files changed, 29 insertions(+), 27 deletions(-) diff --git a/plugins/canvas/src/CanvasFeatureRenderer/FeatureGlyph.ts b/plugins/canvas/src/CanvasFeatureRenderer/FeatureGlyph.ts index 125efa31b4..214eb55c23 100644 --- a/plugins/canvas/src/CanvasFeatureRenderer/FeatureGlyph.ts +++ b/plugins/canvas/src/CanvasFeatureRenderer/FeatureGlyph.ts @@ -18,6 +18,8 @@ export interface FeatureRect { } export interface LaidOutFeatureRect extends FeatureRect { + label?: { text: string; offsetY: number } + description?: { text: string; offsetY: number } t: number h: number } diff --git a/plugins/canvas/src/CanvasFeatureRenderer/FeatureGlyphs/Box.ts b/plugins/canvas/src/CanvasFeatureRenderer/FeatureGlyphs/Box.ts index bfc0294537..6c56dc4063 100644 --- a/plugins/canvas/src/CanvasFeatureRenderer/FeatureGlyphs/Box.ts +++ b/plugins/canvas/src/CanvasFeatureRenderer/FeatureGlyphs/Box.ts @@ -43,8 +43,8 @@ export default class Box extends FeatureGlyph { baseline: 'middle', w: measureText(text), h: 10, // FIXME - xOffset: 0, - yOffset: 0, + offsetX: 0, + offsetY: 0, } } @@ -56,7 +56,7 @@ export default class Box extends FeatureGlyph { text: text, w: measureText(text), h: 10, // FIXME - yOffset: 0, + offsetY: 0, } } @@ -98,7 +98,7 @@ export default class Box extends FeatureGlyph { if (label) { fRect.h += label.h fRect.w = Math.max(label.w, fRect.w) - label.yOffset = fRect.h + label.offsetY = fRect.h } // maybe get the feature's description if available, and // update the layout box accordingly @@ -109,7 +109,7 @@ export default class Box extends FeatureGlyph { if (description) { fRect.h += description.h fRect.w = Math.max(description.w, fRect.w) - description.yOffset = fRect.h // -marginBottom removed in jb2 + description.offsetY = fRect.h // -marginBottom removed in jb2 } return { ...fRect, description, label } } @@ -172,18 +172,23 @@ export default class Box extends FeatureGlyph { }, ) { const { regionStart, record, offsetPx } = props - const { - start, - end, - l, - t, - label: { text, yOffset }, - } = record - - if (start < regionStart && regionStart < end) { - ctx.fillText(text, offsetPx, t + yOffset) - } else { - ctx.fillText(text, l, t + yOffset) + const { start, end, l, t, label, description } = record + + function renderText({ text, offsetY }: { text: string; offsetY: number }) { + if (start < regionStart && regionStart < end) { + ctx.fillText(text, offsetPx, t + offsetY) + } else { + ctx.fillText(text, l, t + offsetY) + } + } + if (label) { + ctx.fillStyle = 'black' + renderText(label) + } + + if (description) { + ctx.fillStyle = 'blue' + renderText(description) } } } diff --git a/plugins/canvas/src/CanvasFeatureRenderer/FeatureGlyphs/Gene.ts b/plugins/canvas/src/CanvasFeatureRenderer/FeatureGlyphs/Gene.ts index 9628e71845..938c8dc971 100644 --- a/plugins/canvas/src/CanvasFeatureRenderer/FeatureGlyphs/Gene.ts +++ b/plugins/canvas/src/CanvasFeatureRenderer/FeatureGlyphs/Gene.ts @@ -65,8 +65,8 @@ export default class Gene extends BoxGlyph { // if (transcriptLabel.h > subRect.h) { // subRect.h = transcriptLabel.h // } - // transcriptLabel.yOffset = Math.floor(subRect.h / 2) - // transcriptLabel.xOffset = 0 + // transcriptLabel.offsetY = Math.floor(subRect.h / 2) + // transcriptLabel.offsetX = 0 // } fRect.subRects.push(subRect) // { ...subRect, label: transcriptLabel }) @@ -110,6 +110,7 @@ export default class Gene extends BoxGlyph { ? new NoncodingGlyph() : new BoxGlyph() } + renderFeature( context: CanvasRenderingContext2D, viewInfo: ViewInfo, diff --git a/plugins/canvas/src/CanvasFeatureRenderer/components/CanvasFeatureRendering.tsx b/plugins/canvas/src/CanvasFeatureRenderer/components/CanvasFeatureRendering.tsx index a2fb8edafa..decb252315 100644 --- a/plugins/canvas/src/CanvasFeatureRenderer/components/CanvasFeatureRendering.tsx +++ b/plugins/canvas/src/CanvasFeatureRenderer/components/CanvasFeatureRendering.tsx @@ -11,6 +11,7 @@ import { postDraw } from '../CanvasFeatureRenderer' // locals import BoxGlyph from '../FeatureGlyphs/Box' import GeneGlyph from '../FeatureGlyphs/Gene' +import { LaidOutFeatureRect } from '../FeatureGlyph' // used so that user can click-away-from-feature below the laid out features // (issue #1248) @@ -23,14 +24,7 @@ function CanvasRendering(props: { height: number regions: Region[] bpPerPx: number - layoutRecords: { - start: number - end: number - t: number - l: number - description: { text: string; yOffset: number } - label: { text: string; yOffset: number } - }[] + layoutRecords: LaidOutFeatureRect[] onMouseMove?: (event: React.MouseEvent, featureId: string | undefined) => void }) { const { From 3ee3426c0becec1d59cdca5c62944a8cc2f9b81b Mon Sep 17 00:00:00 2001 From: Colin Date: Mon, 25 Oct 2021 14:35:32 -0400 Subject: [PATCH 11/28] Misc --- .../CanvasFeatureRenderer.ts | 25 ++++++++++--------- .../FeatureGlyphs/Box.ts | 7 +++--- .../components/CanvasFeatureRendering.tsx | 1 + 3 files changed, 18 insertions(+), 15 deletions(-) diff --git a/plugins/canvas/src/CanvasFeatureRenderer/CanvasFeatureRenderer.ts b/plugins/canvas/src/CanvasFeatureRenderer/CanvasFeatureRenderer.ts index f2247890a4..c9a7d179cf 100644 --- a/plugins/canvas/src/CanvasFeatureRenderer/CanvasFeatureRenderer.ts +++ b/plugins/canvas/src/CanvasFeatureRenderer/CanvasFeatureRenderer.ts @@ -82,14 +82,17 @@ export default class CanvasRenderer extends BoxRendererType { if (props.exportSVG) { postDraw({ ctx, - layoutRecords: layoutRecords.map(f => ({ - label: f.label, - description: f.description, - l: f.l, - t: f.t, - start: f.f.get('start'), - end: f.f.get('end'), - })), + layoutRecords: layoutRecords.map( + ({ glyph, label, description, l, f, t }) => ({ + label, + description, + l, + t, + glyph, + start: f.get('start'), + end: f.get('end'), + }), + ), offsetPx: 0, ...props, }) @@ -160,12 +163,10 @@ export function postDraw({ regions, }: { ctx: CanvasRenderingContext2D - regions: { start: number }[] + regions: { start: number; end: number; reversed: boolean }[] offsetPx: number layoutRecords: LaidOutFeatureRectWithGlyph[] }) { - const [region] = regions - ctx.fillStyle = 'black' ctx.font = '10px sans-serif' layoutRecords @@ -173,7 +174,7 @@ export function postDraw({ .forEach(record => { record.glyph.postDraw(ctx, { record, - regionStart: region.start, + regions, offsetPx, }) }) diff --git a/plugins/canvas/src/CanvasFeatureRenderer/FeatureGlyphs/Box.ts b/plugins/canvas/src/CanvasFeatureRenderer/FeatureGlyphs/Box.ts index 6c56dc4063..01c52ba726 100644 --- a/plugins/canvas/src/CanvasFeatureRenderer/FeatureGlyphs/Box.ts +++ b/plugins/canvas/src/CanvasFeatureRenderer/FeatureGlyphs/Box.ts @@ -167,15 +167,16 @@ export default class Box extends FeatureGlyph { ctx: CanvasRenderingContext2D, props: { record: LaidOutFeatureRect - regionStart: number + regions: { start: number; end: number; reversed: boolean }[] offsetPx: number }, ) { - const { regionStart, record, offsetPx } = props + const { regions, record, offsetPx } = props const { start, end, l, t, label, description } = record + const [region] = regions function renderText({ text, offsetY }: { text: string; offsetY: number }) { - if (start < regionStart && regionStart < end) { + if (start < region.start && region.start < end) { ctx.fillText(text, offsetPx, t + offsetY) } else { ctx.fillText(text, l, t + offsetY) diff --git a/plugins/canvas/src/CanvasFeatureRenderer/components/CanvasFeatureRendering.tsx b/plugins/canvas/src/CanvasFeatureRenderer/components/CanvasFeatureRendering.tsx index decb252315..1fb407b49c 100644 --- a/plugins/canvas/src/CanvasFeatureRenderer/components/CanvasFeatureRendering.tsx +++ b/plugins/canvas/src/CanvasFeatureRenderer/components/CanvasFeatureRendering.tsx @@ -82,6 +82,7 @@ function CanvasRendering(props: { regions: [{ start: viewStart }], }) }, [layoutRecords, viewStart, offsetPx]) + useEffect(() => { const canvas = highlightOverlayCanvas.current if (!canvas) { From 9884195a6e2c3b9c6cdde7457f3ddd23bdfbdcd4 Mon Sep 17 00:00:00 2001 From: Colin Date: Mon, 24 Jan 2022 16:09:29 -0700 Subject: [PATCH 12/28] Lint --- .../CanvasFeatureRenderer/components/CanvasFeatureRendering.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/plugins/canvas/src/CanvasFeatureRenderer/components/CanvasFeatureRendering.tsx b/plugins/canvas/src/CanvasFeatureRenderer/components/CanvasFeatureRendering.tsx index 1fb407b49c..68851e44bf 100644 --- a/plugins/canvas/src/CanvasFeatureRenderer/components/CanvasFeatureRendering.tsx +++ b/plugins/canvas/src/CanvasFeatureRenderer/components/CanvasFeatureRendering.tsx @@ -205,8 +205,10 @@ function CanvasRendering(props: { function callMouseHandler(handlerName: string, event: React.MouseEvent) { // @ts-ignore + // eslint-disable-next-line react/destructuring-assignment const featureHandler = props[`onFeature${handlerName}`] // @ts-ignore + // eslint-disable-next-line react/destructuring-assignment const canvasHandler = props[`on${handlerName}`] if (featureHandler && featureIdUnderMouse) { featureHandler(event, featureIdUnderMouse) From 5899a289debadbb1bf2107229b3aa0163912267d Mon Sep 17 00:00:00 2001 From: Colin Date: Wed, 2 Feb 2022 14:19:52 -0700 Subject: [PATCH 13/28] Typescripting --- packages/core/util/index.ts | 1 + .../components/{Segments.js => Segments.tsx} | 45 +++++++------------ 2 files changed, 16 insertions(+), 30 deletions(-) rename plugins/svg/src/SvgFeatureRenderer/components/{Segments.js => Segments.tsx} (70%) diff --git a/packages/core/util/index.ts b/packages/core/util/index.ts index 7b32618fb3..76fecc05e8 100644 --- a/packages/core/util/index.ts +++ b/packages/core/util/index.ts @@ -29,6 +29,7 @@ export * from './aborting' export * from './when' export * from './range' export { SimpleFeature, isFeature } +export type { Feature } export * from './offscreenCanvasPonyfill' export * from './offscreenCanvasUtils' diff --git a/plugins/svg/src/SvgFeatureRenderer/components/Segments.js b/plugins/svg/src/SvgFeatureRenderer/components/Segments.tsx similarity index 70% rename from plugins/svg/src/SvgFeatureRenderer/components/Segments.js rename to plugins/svg/src/SvgFeatureRenderer/components/Segments.tsx index 548841d126..5441f56dc2 100644 --- a/plugins/svg/src/SvgFeatureRenderer/components/Segments.js +++ b/plugins/svg/src/SvgFeatureRenderer/components/Segments.tsx @@ -1,11 +1,20 @@ -import { readConfObject } from '@jbrowse/core/configuration' -import { PropTypes as CommonPropTypes } from '@jbrowse/core/util/types/mst' +import React from 'react' +import { + readConfObject, + AnyConfigurationModel, +} from '@jbrowse/core/configuration' +import { Feature } from '@jbrowse/core/util' import { emphasize } from '@jbrowse/core/util/color' import { observer } from 'mobx-react' -import PropTypes from 'prop-types' -import React from 'react' -function Segments(props) { +function Segments(props: { + feature: Feature + featureLayout: any + selected: string + config: AnyConfigurationModel + subfeatures: Feature[] + reversed: boolean +}) { const { feature, featureLayout, @@ -49,7 +58,7 @@ function Segments(props) { ? `rotate(180,${left + width / 2},${top + height / 2})` : undefined } - points={points} + points={points.toString()} stroke={selected ? emphasizedColor2 : color2} /> {subfeatures.map(subfeature => { @@ -74,28 +83,4 @@ function Segments(props) { ) } -Segments.propTypes = { - feature: PropTypes.shape({ - id: PropTypes.func.isRequired, - get: PropTypes.func.isRequired, - }).isRequired, - featureLayout: PropTypes.shape({ - absolute: PropTypes.shape({ - top: PropTypes.number.isRequired, - left: PropTypes.number.isRequired, - width: PropTypes.number.isRequired, - height: PropTypes.number.isRequired, - }), - getSubRecord: PropTypes.func.isRequired, - }).isRequired, - selected: PropTypes.bool, - config: CommonPropTypes.ConfigSchema.isRequired, - reversed: PropTypes.bool, -} - -Segments.defaultProps = { - selected: false, - reversed: false, -} - export default observer(Segments) From 5fe0d5d0da9ef4657e3dc7e37e62f6347d97dab4 Mon Sep 17 00:00:00 2001 From: Colin Date: Thu, 3 Feb 2022 13:39:08 -0700 Subject: [PATCH 14/28] Back to main --- .../SvgFeatureRenderer/components/Segments.js | 101 ++++++++++++++++++ 1 file changed, 101 insertions(+) create mode 100644 plugins/svg/src/SvgFeatureRenderer/components/Segments.js diff --git a/plugins/svg/src/SvgFeatureRenderer/components/Segments.js b/plugins/svg/src/SvgFeatureRenderer/components/Segments.js new file mode 100644 index 0000000000..548841d126 --- /dev/null +++ b/plugins/svg/src/SvgFeatureRenderer/components/Segments.js @@ -0,0 +1,101 @@ +import { readConfObject } from '@jbrowse/core/configuration' +import { PropTypes as CommonPropTypes } from '@jbrowse/core/util/types/mst' +import { emphasize } from '@jbrowse/core/util/color' +import { observer } from 'mobx-react' +import PropTypes from 'prop-types' +import React from 'react' + +function Segments(props) { + const { + feature, + featureLayout, + selected, + config, + reversed, + // some subfeatures may be computed e.g. makeUTRs, + // so these are passed as a prop + // eslint-disable-next-line react/prop-types + subfeatures: subfeaturesProp, + } = props + + const subfeatures = subfeaturesProp || feature.get('subfeatures') + const color2 = readConfObject(config, 'color2', { feature }) + let emphasizedColor2 + try { + emphasizedColor2 = emphasize(color2, 0.3) + } catch (error) { + emphasizedColor2 = color2 + } + const { left, top, width, height } = featureLayout.absolute + const points = [ + [left, top + height / 2], + [left + width, top + height / 2], + ] + const strand = feature.get('strand') + if (strand) { + points.push( + [left + width - height / 4, top + height / 4], + [left + width - height / 4, top + 3 * (height / 4)], + [left + width, top + height / 2], + ) + } + + return ( + <> + 0)) + ? `rotate(180,${left + width / 2},${top + height / 2})` + : undefined + } + points={points} + stroke={selected ? emphasizedColor2 : color2} + /> + {subfeatures.map(subfeature => { + const subfeatureId = String(subfeature.id()) + const subfeatureLayout = featureLayout.getSubRecord(subfeatureId) + // This subfeature got filtered out + if (!subfeatureLayout) { + return null + } + const { GlyphComponent } = subfeatureLayout.data + return ( + + ) + })} + + ) +} + +Segments.propTypes = { + feature: PropTypes.shape({ + id: PropTypes.func.isRequired, + get: PropTypes.func.isRequired, + }).isRequired, + featureLayout: PropTypes.shape({ + absolute: PropTypes.shape({ + top: PropTypes.number.isRequired, + left: PropTypes.number.isRequired, + width: PropTypes.number.isRequired, + height: PropTypes.number.isRequired, + }), + getSubRecord: PropTypes.func.isRequired, + }).isRequired, + selected: PropTypes.bool, + config: CommonPropTypes.ConfigSchema.isRequired, + reversed: PropTypes.bool, +} + +Segments.defaultProps = { + selected: false, + reversed: false, +} + +export default observer(Segments) From dd13abf027943e5d116f4c4014bd7906199ec506 Mon Sep 17 00:00:00 2001 From: Colin Date: Thu, 3 Feb 2022 13:39:15 -0700 Subject: [PATCH 15/28] More canvas --- .../src/CanvasFeatureRenderer/FeatureGlyph.ts | 28 ++++++++++++------- .../FeatureGlyphs/Box.ts | 14 ++++++---- .../FeatureGlyphs/Gene.ts | 11 ++++++-- 3 files changed, 34 insertions(+), 19 deletions(-) diff --git a/plugins/canvas/src/CanvasFeatureRenderer/FeatureGlyph.ts b/plugins/canvas/src/CanvasFeatureRenderer/FeatureGlyph.ts index 214eb55c23..94d083be72 100644 --- a/plugins/canvas/src/CanvasFeatureRenderer/FeatureGlyph.ts +++ b/plugins/canvas/src/CanvasFeatureRenderer/FeatureGlyph.ts @@ -1,7 +1,8 @@ -import { readConfObject } from '@jbrowse/core/configuration' -import { Feature } from '@jbrowse/core/util/simpleFeature' -import { AnyConfigurationModel } from '@jbrowse/core/configuration/configurationSchema' -import { bpSpanPx } from '@jbrowse/core/util' +import { + readConfObject, + AnyConfigurationModel, +} from '@jbrowse/core/configuration' +import { bpSpanPx, Feature } from '@jbrowse/core/util' import { Region } from '@jbrowse/core/util/types' import { BaseLayout } from '@jbrowse/core/util/layouts/BaseLayout' @@ -22,9 +23,17 @@ export interface LaidOutFeatureRect extends FeatureRect { description?: { text: string; offsetY: number } t: number h: number + rect: any } -export default class FeatureGlyph { +interface Rect { + l: number + r: number + t: number + w: number +} + +export default abstract class FeatureGlyph { layoutFeature( viewArgs: ViewInfo, layout: BaseLayout, @@ -45,12 +54,11 @@ export default class FeatureGlyph { return { ...fRect, f: feature, t: top } } - // stub - renderFeature( + abstract renderFeature( context: CanvasRenderingContext2D, viewInfo: ViewInfo, fRect: LaidOutFeatureRect, - ) {} + ): void getFeatureRectangle(viewInfo: ViewInfo, feature: Feature) { const { region, bpPerPx } = viewInfo @@ -67,11 +75,11 @@ export default class FeatureGlyph { h: this.getFeatureHeight(viewInfo, feature), f: feature, t: 0, + rect: undefined as Rect | undefined, } } getFeatureHeight(viewInfo: ViewInfo, feature: Feature) { - const { config } = viewInfo - return readConfObject(config, 'height', { feature }) + return readConfObject(viewInfo.config, 'height', { feature }) } } diff --git a/plugins/canvas/src/CanvasFeatureRenderer/FeatureGlyphs/Box.ts b/plugins/canvas/src/CanvasFeatureRenderer/FeatureGlyphs/Box.ts index 01c52ba726..fa8e823900 100644 --- a/plugins/canvas/src/CanvasFeatureRenderer/FeatureGlyphs/Box.ts +++ b/plugins/canvas/src/CanvasFeatureRenderer/FeatureGlyphs/Box.ts @@ -65,8 +65,6 @@ export default class Box extends FeatureGlyph { const { l, h } = fRect const w = Math.max(fRect.w, 2) - fRect.rect = { h, l, w, t: 0 } - fRect.w = w // fixme maybe // const i = { width: 16 } @@ -77,7 +75,11 @@ export default class Box extends FeatureGlyph { // fRect.w += i.width // } - return this.expandRectangleWithLabels(viewArgs, feature, fRect) + return this.expandRectangleWithLabels(viewArgs, feature, { + ...fRect, + w, + rect: { h, l, w, t: 0 }, + }) } // given an under-construction feature layout rectangle, expand it to @@ -87,11 +89,11 @@ export default class Box extends FeatureGlyph { feature: Feature, fRect: FeatureRect & { h: number - rect?: { l: number; w: number; h: number } + t?: number + rect?: { l: number; w: number; h: number; t: number } }, ) { - // maybe get the feature's name, and update the layout box - // accordingly + // maybe get the feature's name, and update the layout box accordingly const { config } = viewInfo const showLabels = readConfObject(config, 'showLabels') const label = showLabels ? this.makeFeatureLabel(feature, fRect) : undefined diff --git a/plugins/canvas/src/CanvasFeatureRenderer/FeatureGlyphs/Gene.ts b/plugins/canvas/src/CanvasFeatureRenderer/FeatureGlyphs/Gene.ts index 938c8dc971..a67f604186 100644 --- a/plugins/canvas/src/CanvasFeatureRenderer/FeatureGlyphs/Gene.ts +++ b/plugins/canvas/src/CanvasFeatureRenderer/FeatureGlyphs/Gene.ts @@ -9,7 +9,7 @@ import { ViewInfo, LaidOutFeatureRect } from '../FeatureGlyph' interface FeatureRectWithGlyph extends LaidOutFeatureRect { label: any - rect: { h: number } + rect: { h: number; t: number } } interface LaidOutFeatureRectWithSubRects extends LaidOutFeatureRect { subRects?: FeatureRectWithGlyph[] @@ -48,11 +48,16 @@ export default class Gene extends BoxGlyph { subfeatures.forEach(sub => { const glyph = this.getSubGlyph(sub) const subRect = glyph.getFeatureRectangle(subArgs, sub) + const rect = subRect.rect + if (!rect) { + console.warn('feature not laid out') + return + } const padding = 1 const newTop = fRect.h ? fRect.h + padding : 0 subRect.t = newTop - subRect.rect.t = newTop + rect.t = newTop // const transcriptLabel = this.makeSideLabel( // this.getFeatureLabel(sub), @@ -72,7 +77,7 @@ export default class Gene extends BoxGlyph { fRect.subRects.push(subRect) // { ...subRect, label: transcriptLabel }) fRect.r = Math.max(fRect.r, subRect.l + subRect.w - 1) fRect.l = Math.min(fRect.l, subRect.l) - fRect.h = subRect.t + subRect.rect.h + padding + fRect.h = subRect.t + rect.h + padding }) // calculate the width From abbc0f19b278e718dce51d0dc0c3bac390c96056 Mon Sep 17 00:00:00 2001 From: Colin Date: Thu, 3 Feb 2022 13:52:27 -0700 Subject: [PATCH 16/28] Temp --- .../src/CanvasFeatureRenderer/FeatureGlyph.ts | 1 - .../FeatureGlyphs/Box.ts | 7 +- .../FeatureGlyphs/Gene.ts | 14 +-- .../components/Segments.tsx | 86 ------------------- 4 files changed, 10 insertions(+), 98 deletions(-) delete mode 100644 plugins/svg/src/SvgFeatureRenderer/components/Segments.tsx diff --git a/plugins/canvas/src/CanvasFeatureRenderer/FeatureGlyph.ts b/plugins/canvas/src/CanvasFeatureRenderer/FeatureGlyph.ts index 94d083be72..fb37e558f1 100644 --- a/plugins/canvas/src/CanvasFeatureRenderer/FeatureGlyph.ts +++ b/plugins/canvas/src/CanvasFeatureRenderer/FeatureGlyph.ts @@ -28,7 +28,6 @@ export interface LaidOutFeatureRect extends FeatureRect { interface Rect { l: number - r: number t: number w: number } diff --git a/plugins/canvas/src/CanvasFeatureRenderer/FeatureGlyphs/Box.ts b/plugins/canvas/src/CanvasFeatureRenderer/FeatureGlyphs/Box.ts index fa8e823900..e0216f2477 100644 --- a/plugins/canvas/src/CanvasFeatureRenderer/FeatureGlyphs/Box.ts +++ b/plugins/canvas/src/CanvasFeatureRenderer/FeatureGlyphs/Box.ts @@ -63,7 +63,6 @@ export default class Box extends FeatureGlyph { getFeatureRectangle(viewArgs: ViewInfo, feature: Feature) { const fRect = super.getFeatureRectangle(viewArgs, feature) - const { l, h } = fRect const w = Math.max(fRect.w, 2) // fixme maybe @@ -78,7 +77,7 @@ export default class Box extends FeatureGlyph { return this.expandRectangleWithLabels(viewArgs, feature, { ...fRect, w, - rect: { h, l, w, t: 0 }, + rect: { ...fRect, w, t: 0 }, }) } @@ -89,8 +88,8 @@ export default class Box extends FeatureGlyph { feature: Feature, fRect: FeatureRect & { h: number - t?: number - rect?: { l: number; w: number; h: number; t: number } + t: number + rect?: { l: number; w: number; h: number; t: number; r: number } }, ) { // maybe get the feature's name, and update the layout box accordingly diff --git a/plugins/canvas/src/CanvasFeatureRenderer/FeatureGlyphs/Gene.ts b/plugins/canvas/src/CanvasFeatureRenderer/FeatureGlyphs/Gene.ts index a67f604186..10423eca7d 100644 --- a/plugins/canvas/src/CanvasFeatureRenderer/FeatureGlyphs/Gene.ts +++ b/plugins/canvas/src/CanvasFeatureRenderer/FeatureGlyphs/Gene.ts @@ -19,11 +19,9 @@ export default class Gene extends BoxGlyph { getFeatureRectangle(viewArgs: ViewInfo, feature: Feature) { // we need to lay out rects for each of the subfeatures const subArgs = viewArgs - // subArgs.showDescriptions = false - // subArgs.showLabels = false const subfeatures = feature.children() - // if this gene weirdly has no subfeatures, just render as a box + // if this gene has no subfeatures, just render as a box if (!subfeatures?.length) { return super.getFeatureRectangle(viewArgs, feature) } @@ -56,8 +54,6 @@ export default class Gene extends BoxGlyph { const padding = 1 const newTop = fRect.h ? fRect.h + padding : 0 - subRect.t = newTop - rect.t = newTop // const transcriptLabel = this.makeSideLabel( // this.getFeatureLabel(sub), @@ -74,10 +70,14 @@ export default class Gene extends BoxGlyph { // transcriptLabel.offsetX = 0 // } - fRect.subRects.push(subRect) // { ...subRect, label: transcriptLabel }) + fRect.subRects.push({ + ...subRect, + t: newTop, + rect: { ...rect, t: newTop }, + }) fRect.r = Math.max(fRect.r, subRect.l + subRect.w - 1) fRect.l = Math.min(fRect.l, subRect.l) - fRect.h = subRect.t + rect.h + padding + fRect.h = newTop + rect.h + padding }) // calculate the width diff --git a/plugins/svg/src/SvgFeatureRenderer/components/Segments.tsx b/plugins/svg/src/SvgFeatureRenderer/components/Segments.tsx deleted file mode 100644 index 5441f56dc2..0000000000 --- a/plugins/svg/src/SvgFeatureRenderer/components/Segments.tsx +++ /dev/null @@ -1,86 +0,0 @@ -import React from 'react' -import { - readConfObject, - AnyConfigurationModel, -} from '@jbrowse/core/configuration' -import { Feature } from '@jbrowse/core/util' -import { emphasize } from '@jbrowse/core/util/color' -import { observer } from 'mobx-react' - -function Segments(props: { - feature: Feature - featureLayout: any - selected: string - config: AnyConfigurationModel - subfeatures: Feature[] - reversed: boolean -}) { - const { - feature, - featureLayout, - selected, - config, - reversed, - // some subfeatures may be computed e.g. makeUTRs, - // so these are passed as a prop - // eslint-disable-next-line react/prop-types - subfeatures: subfeaturesProp, - } = props - - const subfeatures = subfeaturesProp || feature.get('subfeatures') - const color2 = readConfObject(config, 'color2', { feature }) - let emphasizedColor2 - try { - emphasizedColor2 = emphasize(color2, 0.3) - } catch (error) { - emphasizedColor2 = color2 - } - const { left, top, width, height } = featureLayout.absolute - const points = [ - [left, top + height / 2], - [left + width, top + height / 2], - ] - const strand = feature.get('strand') - if (strand) { - points.push( - [left + width - height / 4, top + height / 4], - [left + width - height / 4, top + 3 * (height / 4)], - [left + width, top + height / 2], - ) - } - - return ( - <> - 0)) - ? `rotate(180,${left + width / 2},${top + height / 2})` - : undefined - } - points={points.toString()} - stroke={selected ? emphasizedColor2 : color2} - /> - {subfeatures.map(subfeature => { - const subfeatureId = String(subfeature.id()) - const subfeatureLayout = featureLayout.getSubRecord(subfeatureId) - // This subfeature got filtered out - if (!subfeatureLayout) { - return null - } - const { GlyphComponent } = subfeatureLayout.data - return ( - - ) - })} - - ) -} - -export default observer(Segments) From e5312f44dc8c865e87db287410c1b85eaaae39bd Mon Sep 17 00:00:00 2001 From: Colin Date: Sat, 5 Feb 2022 12:15:37 -0700 Subject: [PATCH 17/28] Stuff --- .../CanvasFeatureRenderer.ts | 50 +++++++++---------- .../src/CanvasFeatureRenderer/FeatureGlyph.ts | 5 ++ .../FeatureGlyphs/Box.ts | 12 ++--- .../FeatureGlyphs/ProcessedTranscript.ts | 2 +- .../FeatureGlyphs/Segments.ts | 3 +- .../FeatureGlyphs/UnprocessedTranscript.ts | 2 +- 6 files changed, 37 insertions(+), 37 deletions(-) diff --git a/plugins/canvas/src/CanvasFeatureRenderer/CanvasFeatureRenderer.ts b/plugins/canvas/src/CanvasFeatureRenderer/CanvasFeatureRenderer.ts index c9a7d179cf..43b6447281 100644 --- a/plugins/canvas/src/CanvasFeatureRenderer/CanvasFeatureRenderer.ts +++ b/plugins/canvas/src/CanvasFeatureRenderer/CanvasFeatureRenderer.ts @@ -6,15 +6,19 @@ import BoxRendererType, { ResultsSerialized, ResultsDeserialized, } from '@jbrowse/core/pluggableElementTypes/renderers/BoxRendererType' -import { Feature } from '@jbrowse/core/util/simpleFeature' +import { Region } from '@jbrowse/core/util/types' import { BaseLayout } from '@jbrowse/core/util/layouts/BaseLayout' -import { iterMap } from '@jbrowse/core/util' +import { iterMap, Feature } from '@jbrowse/core/util' import { renderToAbstractCanvas } from '@jbrowse/core/util/offscreenCanvasUtils' // locals import BoxGlyph from './FeatureGlyphs/Box' import GeneGlyph from './FeatureGlyphs/Gene' -import { LaidOutFeatureRect } from './FeatureGlyph' +import { PostDrawFeatureRect, LaidOutFeatureRect } from './FeatureGlyph' + +export interface PostDrawFeatureRectWithGlyph extends PostDrawFeatureRect { + glyph: BoxGlyph +} export interface LaidOutFeatureRectWithGlyph extends LaidOutFeatureRect { glyph: BoxGlyph @@ -82,17 +86,12 @@ export default class CanvasRenderer extends BoxRendererType { if (props.exportSVG) { postDraw({ ctx, - layoutRecords: layoutRecords.map( - ({ glyph, label, description, l, f, t }) => ({ - label, - description, - l, - t, - glyph, - start: f.get('start'), - end: f.get('end'), - }), - ), + layoutRecords: layoutRecords.map(rec => ({ + ...rec, + type: rec.f.get('type'), + start: rec.f.get('start'), + end: rec.f.get('end'), + })), offsetPx: 0, ...props, }) @@ -138,14 +137,11 @@ export default class CanvasRenderer extends BoxRendererType { return { ...results, ...res, - layoutRecords: layoutRecords.map(({ f, l, t, label, description }) => ({ - label, - description, - l, - t, - type: f.get('type'), - start: f.get('start'), - end: f.get('end'), + layoutRecords: layoutRecords.map(rec => ({ + ...rec, + type: rec.f.get('type'), + start: rec.f.get('start'), + end: rec.f.get('end'), })), features, layout, @@ -163,19 +159,19 @@ export function postDraw({ regions, }: { ctx: CanvasRenderingContext2D - regions: { start: number; end: number; reversed: boolean }[] + regions: Region[] offsetPx: number - layoutRecords: LaidOutFeatureRectWithGlyph[] + layoutRecords: PostDrawFeatureRectWithGlyph[] }) { ctx.fillStyle = 'black' ctx.font = '10px sans-serif' layoutRecords .filter(f => !!f) - .forEach(record => { + .forEach(record => record.glyph.postDraw(ctx, { record, regions, offsetPx, - }) - }) + }), + ) } diff --git a/plugins/canvas/src/CanvasFeatureRenderer/FeatureGlyph.ts b/plugins/canvas/src/CanvasFeatureRenderer/FeatureGlyph.ts index fb37e558f1..524698ae50 100644 --- a/plugins/canvas/src/CanvasFeatureRenderer/FeatureGlyph.ts +++ b/plugins/canvas/src/CanvasFeatureRenderer/FeatureGlyph.ts @@ -26,6 +26,11 @@ export interface LaidOutFeatureRect extends FeatureRect { rect: any } +export interface PostDrawFeatureRect extends LaidOutFeatureRect { + start: number + end: number +} + interface Rect { l: number t: number diff --git a/plugins/canvas/src/CanvasFeatureRenderer/FeatureGlyphs/Box.ts b/plugins/canvas/src/CanvasFeatureRenderer/FeatureGlyphs/Box.ts index e0216f2477..5cfbe9413d 100644 --- a/plugins/canvas/src/CanvasFeatureRenderer/FeatureGlyphs/Box.ts +++ b/plugins/canvas/src/CanvasFeatureRenderer/FeatureGlyphs/Box.ts @@ -1,12 +1,12 @@ -import { measureText } from '@jbrowse/core/util' -import { Feature } from '@jbrowse/core/util/simpleFeature' +import { measureText, bpSpanPx, Feature } from '@jbrowse/core/util' +import { Region } from '@jbrowse/core/util/types' import { readConfObject } from '@jbrowse/core/configuration' -import { bpSpanPx } from '@jbrowse/core/util' // locals import FeatureGlyph, { ViewInfo, FeatureRect, + PostDrawFeatureRect, LaidOutFeatureRect, } from '../FeatureGlyph' @@ -89,7 +89,7 @@ export default class Box extends FeatureGlyph { fRect: FeatureRect & { h: number t: number - rect?: { l: number; w: number; h: number; t: number; r: number } + rect: { l: number; w: number; h: number; t: number } }, ) { // maybe get the feature's name, and update the layout box accordingly @@ -167,8 +167,8 @@ export default class Box extends FeatureGlyph { postDraw( ctx: CanvasRenderingContext2D, props: { - record: LaidOutFeatureRect - regions: { start: number; end: number; reversed: boolean }[] + record: PostDrawFeatureRect + regions: Region[] offsetPx: number }, ) { diff --git a/plugins/canvas/src/CanvasFeatureRenderer/FeatureGlyphs/ProcessedTranscript.ts b/plugins/canvas/src/CanvasFeatureRenderer/FeatureGlyphs/ProcessedTranscript.ts index c26ddd0e77..f6fe8019ea 100644 --- a/plugins/canvas/src/CanvasFeatureRenderer/FeatureGlyphs/ProcessedTranscript.ts +++ b/plugins/canvas/src/CanvasFeatureRenderer/FeatureGlyphs/ProcessedTranscript.ts @@ -1,5 +1,5 @@ import SegmentsGlyph from './Segments' -import SimpleFeature, { Feature } from '@jbrowse/core/util/simpleFeature' +import { SimpleFeature, Feature } from '@jbrowse/core/util' import { ViewInfo } from '../FeatureGlyph' export default class ProcessedTranscript extends SegmentsGlyph { diff --git a/plugins/canvas/src/CanvasFeatureRenderer/FeatureGlyphs/Segments.ts b/plugins/canvas/src/CanvasFeatureRenderer/FeatureGlyphs/Segments.ts index fa42ef0f08..721d2711f7 100644 --- a/plugins/canvas/src/CanvasFeatureRenderer/FeatureGlyphs/Segments.ts +++ b/plugins/canvas/src/CanvasFeatureRenderer/FeatureGlyphs/Segments.ts @@ -1,5 +1,4 @@ -import { Feature } from '@jbrowse/core/util/simpleFeature' -import { bpSpanPx } from '@jbrowse/core/util' +import { bpSpanPx, Feature } from '@jbrowse/core/util' // locals import { LaidOutFeatureRect, ViewInfo } from '../FeatureGlyph' diff --git a/plugins/canvas/src/CanvasFeatureRenderer/FeatureGlyphs/UnprocessedTranscript.ts b/plugins/canvas/src/CanvasFeatureRenderer/FeatureGlyphs/UnprocessedTranscript.ts index aa7959c1c3..39ea5a4a0e 100644 --- a/plugins/canvas/src/CanvasFeatureRenderer/FeatureGlyphs/UnprocessedTranscript.ts +++ b/plugins/canvas/src/CanvasFeatureRenderer/FeatureGlyphs/UnprocessedTranscript.ts @@ -1,5 +1,5 @@ +import { Feature } from '@jbrowse/core/util' import { ViewInfo } from '../FeatureGlyph' -import { Feature } from '@jbrowse/core/util/simpleFeature' import SegmentsGlyph from './Segments' export default class UnprocessedTranscript extends SegmentsGlyph { From 8e0fe594f9576ff92beeaeb83bd90887dc1392ea Mon Sep 17 00:00:00 2001 From: Colin Date: Sat, 5 Feb 2022 14:39:44 -0700 Subject: [PATCH 18/28] Updates --- .../src/CanvasFeatureRenderer/FeatureGlyph.ts | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/plugins/canvas/src/CanvasFeatureRenderer/FeatureGlyph.ts b/plugins/canvas/src/CanvasFeatureRenderer/FeatureGlyph.ts index 524698ae50..38be53d7c9 100644 --- a/plugins/canvas/src/CanvasFeatureRenderer/FeatureGlyph.ts +++ b/plugins/canvas/src/CanvasFeatureRenderer/FeatureGlyph.ts @@ -18,6 +18,14 @@ export interface FeatureRect { f: Feature } +export interface PreLaidOutFeatureRect { + l: number + w: number + f: Feature + h: number + t: number +} + export interface LaidOutFeatureRect extends FeatureRect { label?: { text: string; offsetY: number } description?: { text: string; offsetY: number } @@ -31,7 +39,7 @@ export interface PostDrawFeatureRect extends LaidOutFeatureRect { end: number } -interface Rect { +export interface Rect { l: number t: number w: number @@ -51,11 +59,8 @@ export default abstract class FeatureGlyph { const startbp = fRect.l / scale + leftBase const endbp = (fRect.l + fRect.w) / scale + leftBase const top = layout.addRect(feature.id(), startbp, endbp, fRect.h) - if (top === null) { - return null - } - return { ...fRect, f: feature, t: top } + return top === null ? null : { ...fRect, f: feature, t: top } } abstract renderFeature( @@ -79,11 +84,10 @@ export default abstract class FeatureGlyph { h: this.getFeatureHeight(viewInfo, feature), f: feature, t: 0, - rect: undefined as Rect | undefined, } } getFeatureHeight(viewInfo: ViewInfo, feature: Feature) { - return readConfObject(viewInfo.config, 'height', { feature }) + return readConfObject(viewInfo.config, 'height', { feature }) as number } } From dc503eabe1f42b03b473fe175894fc717d334b6f Mon Sep 17 00:00:00 2001 From: Colin Date: Sat, 5 Feb 2022 15:18:20 -0700 Subject: [PATCH 19/28] Avoid cleanup warnings lgv --- .../src/LinearGenomeView/components/ExportSvgDialog.tsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/plugins/linear-genome-view/src/LinearGenomeView/components/ExportSvgDialog.tsx b/plugins/linear-genome-view/src/LinearGenomeView/components/ExportSvgDialog.tsx index c40d4adf73..8624ebc4d0 100644 --- a/plugins/linear-genome-view/src/LinearGenomeView/components/ExportSvgDialog.tsx +++ b/plugins/linear-genome-view/src/LinearGenomeView/components/ExportSvgDialog.tsx @@ -86,14 +86,18 @@ export default function ExportSvgDlg({ onClick={async () => { setLoading(true) setError(undefined) + const err = console.error + console.error = (...args) => + args[0]?.match('useLayoutEffect') ? null : err(args) try { await model.exportSvg({ rasterizeLayers }) handleClose() } catch (e) { console.error(e) setError(e) - } finally { setLoading(false) + } finally { + console.error = err } }} > From 2dd9988ac9192b304b5186bd1b5c2b16dfe0362f Mon Sep 17 00:00:00 2001 From: Colin Date: Sun, 6 Feb 2022 08:02:02 -0700 Subject: [PATCH 20/28] Misc --- .../CanvasFeatureRenderer.ts | 20 ++-- .../src/CanvasFeatureRenderer/FeatureGlyph.ts | 1 + .../FeatureGlyphs/Box.ts | 25 ++-- .../FeatureGlyphs/Gene.ts | 108 +++++++++--------- .../components/CanvasFeatureRendering.tsx | 12 +- 5 files changed, 81 insertions(+), 85 deletions(-) diff --git a/plugins/canvas/src/CanvasFeatureRenderer/CanvasFeatureRenderer.ts b/plugins/canvas/src/CanvasFeatureRenderer/CanvasFeatureRenderer.ts index 43b6447281..c89b2075f3 100644 --- a/plugins/canvas/src/CanvasFeatureRenderer/CanvasFeatureRenderer.ts +++ b/plugins/canvas/src/CanvasFeatureRenderer/CanvasFeatureRenderer.ts @@ -86,12 +86,7 @@ export default class CanvasRenderer extends BoxRendererType { if (props.exportSVG) { postDraw({ ctx, - layoutRecords: layoutRecords.map(rec => ({ - ...rec, - type: rec.f.get('type'), - start: rec.f.get('start'), - end: rec.f.get('end'), - })), + layoutRecords: layoutRecords, offsetPx: 0, ...props, }) @@ -138,10 +133,15 @@ export default class CanvasRenderer extends BoxRendererType { ...results, ...res, layoutRecords: layoutRecords.map(rec => ({ - ...rec, - type: rec.f.get('type'), - start: rec.f.get('start'), - end: rec.f.get('end'), + label: rec.label, + description: rec.description, + l: rec.l, + t: rec.t, + f: { + start: rec.f.get('start'), + end: rec.f.get('end'), + type: rec.f.get('type'), + }, })), features, layout, diff --git a/plugins/canvas/src/CanvasFeatureRenderer/FeatureGlyph.ts b/plugins/canvas/src/CanvasFeatureRenderer/FeatureGlyph.ts index 38be53d7c9..bbb98207d0 100644 --- a/plugins/canvas/src/CanvasFeatureRenderer/FeatureGlyph.ts +++ b/plugins/canvas/src/CanvasFeatureRenderer/FeatureGlyph.ts @@ -84,6 +84,7 @@ export default abstract class FeatureGlyph { h: this.getFeatureHeight(viewInfo, feature), f: feature, t: 0, + rect: undefined as Rect | undefined, } } diff --git a/plugins/canvas/src/CanvasFeatureRenderer/FeatureGlyphs/Box.ts b/plugins/canvas/src/CanvasFeatureRenderer/FeatureGlyphs/Box.ts index 5cfbe9413d..d4e467a33f 100644 --- a/plugins/canvas/src/CanvasFeatureRenderer/FeatureGlyphs/Box.ts +++ b/plugins/canvas/src/CanvasFeatureRenderer/FeatureGlyphs/Box.ts @@ -6,6 +6,7 @@ import { readConfObject } from '@jbrowse/core/configuration' import FeatureGlyph, { ViewInfo, FeatureRect, + PreLaidOutFeatureRect, PostDrawFeatureRect, LaidOutFeatureRect, } from '../FeatureGlyph' @@ -62,6 +63,7 @@ export default class Box extends FeatureGlyph { getFeatureRectangle(viewArgs: ViewInfo, feature: Feature) { const fRect = super.getFeatureRectangle(viewArgs, feature) + const { l, h } = fRect const w = Math.max(fRect.w, 2) @@ -77,35 +79,28 @@ export default class Box extends FeatureGlyph { return this.expandRectangleWithLabels(viewArgs, feature, { ...fRect, w, - rect: { ...fRect, w, t: 0 }, + rect: { l, h, w, t: 0 }, }) } // given an under-construction feature layout rectangle, expand it to // accomodate a label and/or a description - expandRectangleWithLabels( - viewInfo: ViewInfo, - feature: Feature, - fRect: FeatureRect & { - h: number - t: number - rect: { l: number; w: number; h: number; t: number } - }, + expandRectangleWithLabels( + view: ViewInfo, + f: Feature, + fRect: T, ) { - // maybe get the feature's name, and update the layout box accordingly - const { config } = viewInfo + const { config } = view const showLabels = readConfObject(config, 'showLabels') - const label = showLabels ? this.makeFeatureLabel(feature, fRect) : undefined + const label = showLabels ? this.makeFeatureLabel(f, fRect) : undefined if (label) { fRect.h += label.h fRect.w = Math.max(label.w, fRect.w) label.offsetY = fRect.h } - // maybe get the feature's description if available, and - // update the layout box accordingly const showDescriptions = readConfObject(config, 'showDescriptions') const description = showDescriptions - ? this.makeFeatureDescriptionLabel(feature, fRect) + ? this.makeFeatureDescriptionLabel(f, fRect) : undefined if (description) { fRect.h += description.h diff --git a/plugins/canvas/src/CanvasFeatureRenderer/FeatureGlyphs/Gene.ts b/plugins/canvas/src/CanvasFeatureRenderer/FeatureGlyphs/Gene.ts index 10423eca7d..84d637ff76 100644 --- a/plugins/canvas/src/CanvasFeatureRenderer/FeatureGlyphs/Gene.ts +++ b/plugins/canvas/src/CanvasFeatureRenderer/FeatureGlyphs/Gene.ts @@ -15,6 +15,8 @@ interface LaidOutFeatureRectWithSubRects extends LaidOutFeatureRect { subRects?: FeatureRectWithGlyph[] } +const getLabel = (f: Feature) => f.get('name') || f.get('id') || '' + export default class Gene extends BoxGlyph { getFeatureRectangle(viewArgs: ViewInfo, feature: Feature) { // we need to lay out rects for each of the subfeatures @@ -26,65 +28,63 @@ export default class Gene extends BoxGlyph { return super.getFeatureRectangle(viewArgs, feature) } - // get the rects for the children - const fRect = { - l: 0, - h: 0, - r: 0, - w: 0, - subRects: [] as FeatureRectWithGlyph[], - f: feature, - t: 0, - } - // sort the children by name - const label = (f: Feature) => f.get('name') || f.get('id') || '' - subfeatures?.sort((a, b) => label(a).localeCompare(label(b))) - - fRect.l = Infinity - fRect.r = -Infinity - - subfeatures.forEach(sub => { - const glyph = this.getSubGlyph(sub) - const subRect = glyph.getFeatureRectangle(subArgs, sub) - const rect = subRect.rect - if (!rect) { - console.warn('feature not laid out') - return - } - - const padding = 1 - const newTop = fRect.h ? fRect.h + padding : 0 - - // const transcriptLabel = this.makeSideLabel( - // this.getFeatureLabel(sub), - // subRect, - // ) - - // if (transcriptLabel) { - // subRect.l -= transcriptLabel.w - // subRect.w += transcriptLabel.w - // if (transcriptLabel.h > subRect.h) { - // subRect.h = transcriptLabel.h - // } - // transcriptLabel.offsetY = Math.floor(subRect.h / 2) - // transcriptLabel.offsetX = 0 - // } - - fRect.subRects.push({ - ...subRect, - t: newTop, - rect: { ...rect, t: newTop }, + let l = Infinity + let r = -Infinity + let h = 0 + + const subRects = subfeatures + .sort((a, b) => getLabel(a).localeCompare(getLabel(b))) + .map(sub => { + const glyph = this.getSubGlyph(sub) + const subRect = glyph.getFeatureRectangle(subArgs, sub) + const rect = subRect.rect + if (!rect) { + console.warn('feature not laid out') + return + } + + const padding = 1 + const newTop = h + padding + + // const transcriptLabel = this.makeSideLabel( + // this.getFeatureLabel(sub), + // subRect, + // ) + + // if (transcriptLabel) { + // subRect.l -= transcriptLabel.w + // subRect.w += transcriptLabel.w + // if (transcriptLabel.h > subRect.h) { + // subRect.h = transcriptLabel.h + // } + // transcriptLabel.offsetY = Math.floor(subRect.h / 2) + // transcriptLabel.offsetX = 0 + // } + + h = newTop + rect.h + padding + r = Math.max(r, subRect.l + subRect.w - 1) + l = Math.min(l, subRect.l) + + return { + ...subRect, + t: newTop, + rect: { ...rect, t: newTop }, + } }) - fRect.r = Math.max(fRect.r, subRect.l + subRect.w - 1) - fRect.l = Math.min(fRect.l, subRect.l) - fRect.h = newTop + rect.h + padding - }) // calculate the width - fRect.w = Math.max(fRect.r - fRect.l + 1, 2) + const w = Math.max(r - l + 1, 2) // expand the fRect to accommodate labels if necessary - return this.expandRectangleWithLabels(viewArgs, feature, fRect) + return this.expandRectangleWithLabels(viewArgs, feature, { + l, + h, + w, + subRects, + f: feature, + rect: { t: 0, l, h, w }, + t: 0, + }) } layoutFeature( diff --git a/plugins/canvas/src/CanvasFeatureRenderer/components/CanvasFeatureRendering.tsx b/plugins/canvas/src/CanvasFeatureRenderer/components/CanvasFeatureRendering.tsx index 68851e44bf..87190129ae 100644 --- a/plugins/canvas/src/CanvasFeatureRenderer/components/CanvasFeatureRendering.tsx +++ b/plugins/canvas/src/CanvasFeatureRenderer/components/CanvasFeatureRendering.tsx @@ -11,7 +11,7 @@ import { postDraw } from '../CanvasFeatureRenderer' // locals import BoxGlyph from '../FeatureGlyphs/Box' import GeneGlyph from '../FeatureGlyphs/Gene' -import { LaidOutFeatureRect } from '../FeatureGlyph' +import { PostDrawFeatureRect } from '../FeatureGlyph' // used so that user can click-away-from-feature below the laid out features // (issue #1248) @@ -24,8 +24,8 @@ function CanvasRendering(props: { height: number regions: Region[] bpPerPx: number - layoutRecords: LaidOutFeatureRect[] - onMouseMove?: (event: React.MouseEvent, featureId: string | undefined) => void + layoutRecords: PostDrawFeatureRect[] + onMouseMove?: (event: React.MouseEvent, featureId?: string) => void }) { const { onMouseMove, @@ -69,14 +69,14 @@ function CanvasRendering(props: { ctx.clearRect(0, 0, canvas.width, canvas.height) postDraw({ ctx, - layoutRecords: layoutRecords.map(f => { + layoutRecords: layoutRecords.map(rec => { let glyph - if (f.type === 'gene') { + if (rec.f.type === 'gene') { glyph = new GeneGlyph() } else { glyph = new BoxGlyph() } - return { ...f, glyph } + return { ...rec, glyph } }), offsetPx, regions: [{ start: viewStart }], From c35a1b542551888ee31ad0fc752c42e6917462f1 Mon Sep 17 00:00:00 2001 From: Colin Date: Mon, 30 Oct 2023 00:59:44 -0400 Subject: [PATCH 21/28] Strong preference for reading frame based coloring scheme Misc Update snaps Collapse intron p1 Properly size linear genome view after creation Bump deps --- .../models/BaseLinearDisplayModel.tsx | 78 +++++++++++++++++-- 1 file changed, 70 insertions(+), 8 deletions(-) diff --git a/plugins/linear-genome-view/src/BaseLinearDisplay/models/BaseLinearDisplayModel.tsx b/plugins/linear-genome-view/src/BaseLinearDisplay/models/BaseLinearDisplayModel.tsx index 717e627452..00766218c1 100644 --- a/plugins/linear-genome-view/src/BaseLinearDisplay/models/BaseLinearDisplayModel.tsx +++ b/plugins/linear-genome-view/src/BaseLinearDisplay/models/BaseLinearDisplayModel.tsx @@ -10,13 +10,20 @@ import { isSelectionContainer, isSessionModelWithWidgets, isFeature, + mergeIntervals, Feature, } from '@jbrowse/core/util' import { BaseBlock } from '@jbrowse/core/util/blockTypes' import CompositeMap from '@jbrowse/core/util/compositeMap' import { getParentRenderProps } from '@jbrowse/core/util/tracks' -import { autorun } from 'mobx' -import { addDisposer, isAlive, types, Instance } from 'mobx-state-tree' +import { autorun, when } from 'mobx' +import { + addDisposer, + isAlive, + getSnapshot, + types, + Instance, +} from 'mobx-state-tree' // icons import MenuOpenIcon from '@mui/icons-material/MenuOpen' @@ -103,8 +110,8 @@ function stateModelFactory() { .views(self => ({ /** * #getter - * how many milliseconds to wait for the display to - * "settle" before re-rendering a block + * how many milliseconds to wait for the display to "settle" before + * re-rendering a block */ get renderDelay() { return 50 @@ -119,8 +126,8 @@ function stateModelFactory() { /** * #getter - * returns a string feature ID if the globally-selected object - * is probably a feature + * returns a string feature ID if the globally-selected object is + * probably a feature */ get selectedFeatureId() { if (isAlive(self)) { @@ -144,8 +151,8 @@ function stateModelFactory() { .views(self => ({ /** * #getter - * a CompositeMap of `featureId -> feature obj` that - * just looks in all the block data for that feature + * a CompositeMap of `featureId -> feature obj` that just looks in all + * the block data for that feature */ get features() { const featureMaps = [] @@ -303,6 +310,23 @@ function stateModelFactory() { * #method */ contextMenuItems(): MenuItem[] { + const { contextMenuFeature } = self + const singleTranscript = contextMenuFeature?.get('subfeatures')?.[0] + const exons = + singleTranscript + ?.get('subfeatures') + ?.filter( + f => f.get('type') === 'exon' || f.get('type') === 'CDS', + ) || [] + const cds = + singleTranscript + ?.get('subfeatures') + ?.filter( + f => f.get('type') === 'exon' || f.get('type') === 'CDS', + ) || [] + + // some GFF3 features have CDS and no exon subfeatures + const subs = exons.length ? exons : cds.length ? cds : [] return [ ...(self.contextMenuFeature ? [ @@ -324,6 +348,44 @@ function stateModelFactory() { } }, }, + ...(exons.length > 0 && contextMenuFeature + ? [ + { + label: 'Collapse introns', + onClick: async () => { + const refName = contextMenuFeature.get('refName') + const view = getContainingView(self) as LGV + const w = 100 + const res = mergeIntervals( + subs.map(f => ({ + refName, + start: f.get('start') - w, + end: f.get('end') + w, + assemblyName: view.assemblyNames[0], + })), + w, + ) + + // need to strip ID before copying view snap + const { id, ...rest } = getSnapshot(view) + const newView = getSession(self).addView( + 'LinearGenomeView', + { + ...rest, + tracks: rest.tracks.map(track => { + const { id, ...rest } = track + return { ...rest } + }), + displayedRegions: res, + }, + ) as LGV + await when(() => newView.initialized) + + newView.showAllRegions() + }, + }, + ] + : []), ] : []), ] From 5d6e0a5ce4208639b3cdce55ed7f43d0e5fac8aa Mon Sep 17 00:00:00 2001 From: Colin Date: Mon, 17 Jun 2024 23:11:17 -0400 Subject: [PATCH 22/28] Misc refactor3 --- .../src/BaseLinearDisplay/components/LinearBlocks.tsx | 2 -- .../src/SvgFeatureRenderer/components/FeatureLabel.tsx | 8 ++++---- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/plugins/linear-genome-view/src/BaseLinearDisplay/components/LinearBlocks.tsx b/plugins/linear-genome-view/src/BaseLinearDisplay/components/LinearBlocks.tsx index 458c7ffddb..2ece5de5e3 100644 --- a/plugins/linear-genome-view/src/BaseLinearDisplay/components/LinearBlocks.tsx +++ b/plugins/linear-genome-view/src/BaseLinearDisplay/components/LinearBlocks.tsx @@ -87,8 +87,6 @@ const RenderedBlocks = observer(function ({ ) }) -export { RenderedBlocks } - const LinearBlocks = observer(function ({ model, }: { diff --git a/plugins/svg/src/SvgFeatureRenderer/components/FeatureLabel.tsx b/plugins/svg/src/SvgFeatureRenderer/components/FeatureLabel.tsx index 39af2b221d..675e8b03a0 100644 --- a/plugins/svg/src/SvgFeatureRenderer/components/FeatureLabel.tsx +++ b/plugins/svg/src/SvgFeatureRenderer/components/FeatureLabel.tsx @@ -61,10 +61,10 @@ const FeatureLabel = observer(function ({ const [labelVisible, setLabelVisible] = useState(exportSVG) const theme = useTheme() - // we use an effect to set the label visible because there can be a - // mismatch between the server and the client after hydration due to the - // floating labels. if we are exporting an SVG we allow it as is though and - // do not use the effect + // we use an effect to set the label visible because there can be a mismatch + // between the server and the client after hydration due to the floating + // labels. if we are exporting an SVG we allow it as is though and do not use + // the effect useEffect(() => { setLabelVisible(true) }, []) From 26dfea7ca40f1ad31e658ddd581b59dee0c54b61 Mon Sep 17 00:00:00 2001 From: Colin Date: Tue, 24 Sep 2024 21:22:35 -0400 Subject: [PATCH 23/28] Global feature layout --- packages/core/util/compositeMap.ts | 4 +- .../CanvasFeatureRenderer.ts | 42 ++++--- .../components/CanvasFeatureRendering.tsx | 58 +-------- .../components/BaseLinearDisplay.tsx | 114 +++++++++++++----- .../models/BaseLinearDisplayModel.tsx | 17 ++- 5 files changed, 129 insertions(+), 106 deletions(-) diff --git a/packages/core/util/compositeMap.ts b/packages/core/util/compositeMap.ts index 687ad4930b..837193e675 100644 --- a/packages/core/util/compositeMap.ts +++ b/packages/core/util/compositeMap.ts @@ -53,13 +53,13 @@ export default class CompositeMap { *[Symbol.iterator]() { for (const key of this.keys()) { - yield [key, this.get(key)] + yield [key, this.get(key)] as const } } *entries() { for (const k of this.keys()) { - yield [k, this.get(k)] + yield [k, this.get(k)] as const } } } diff --git a/plugins/canvas/src/CanvasFeatureRenderer/CanvasFeatureRenderer.ts b/plugins/canvas/src/CanvasFeatureRenderer/CanvasFeatureRenderer.ts index 002f0786fb..48f948034d 100644 --- a/plugins/canvas/src/CanvasFeatureRenderer/CanvasFeatureRenderer.ts +++ b/plugins/canvas/src/CanvasFeatureRenderer/CanvasFeatureRenderer.ts @@ -2,7 +2,7 @@ import BoxRendererType, { RenderArgsDeserialized as BoxRenderArgsDeserialized, } from '@jbrowse/core/pluggableElementTypes/renderers/BoxRendererType' import { BaseLayout } from '@jbrowse/core/util/layouts/BaseLayout' -import { iterMap, Feature } from '@jbrowse/core/util' +import { iterMap, Feature, notEmpty } from '@jbrowse/core/util' import { renderToAbstractCanvas } from '@jbrowse/core/util/offscreenCanvasUtils' // locals @@ -42,8 +42,20 @@ export default class CanvasRenderer extends BoxRendererType { const region = props.regions[0]! const glyph = feature.get('type') === 'gene' ? new GeneGlyph() : new BoxGlyph() - const fRect = glyph.layoutFeature({ region, ...props }, layout, feature) - return fRect ? { ...fRect, glyph } : null + const fRect = glyph.layoutFeature( + { + region, + ...props, + }, + layout, + feature, + ) + return fRect + ? { + ...fRect, + glyph, + } + : null } drawRect( @@ -61,9 +73,9 @@ export default class CanvasRenderer extends BoxRendererType { layoutRecords: LaidOutFeatureRectWithGlyph[], props: RenderArgsDeserializedWithFeaturesAndLayout, ) { - layoutRecords.forEach(fRect => { + for (const fRect of layoutRecords) { this.drawRect(ctx, fRect, props) - }) + } if (props.exportSVG) { postDraw({ @@ -87,7 +99,7 @@ export default class CanvasRenderer extends BoxRendererType { featureMap.values(), feature => this.layoutFeature(feature, layout, renderProps), featureMap.size, - ).filter((f): f is LaidOutFeatureRectWithGlyph => !!f) + ).filter(notEmpty) const width = (region.end - region.start) / bpPerPx const height = Math.max(layout.getTotalHeight(), 1) @@ -142,21 +154,21 @@ export function postDraw({ regions, }: { ctx: CanvasRenderingContext2D - regions: { start: number }[] + regions: { + start: number + }[] offsetPx: number layoutRecords: PostDrawFeatureRectWithGlyph[] }) { ctx.fillStyle = 'black' ctx.font = '10px sans-serif' - layoutRecords - .filter(f => !!f) - .forEach(record => { - record.glyph.postDraw(ctx, { - record, - regions, - offsetPx, - }) + layoutRecords.filter(notEmpty).forEach(record => { + record.glyph.postDraw(ctx, { + record, + regions, + offsetPx, }) + }) } export { diff --git a/plugins/canvas/src/CanvasFeatureRenderer/components/CanvasFeatureRendering.tsx b/plugins/canvas/src/CanvasFeatureRenderer/components/CanvasFeatureRendering.tsx index 2f4b3af8d3..19f5b58cfa 100644 --- a/plugins/canvas/src/CanvasFeatureRenderer/components/CanvasFeatureRendering.tsx +++ b/plugins/canvas/src/CanvasFeatureRenderer/components/CanvasFeatureRendering.tsx @@ -1,19 +1,10 @@ import React, { useRef, useState, useEffect } from 'react' import { Region } from '@jbrowse/core/util/types' import { PrerenderedCanvas } from '@jbrowse/core/ui' -import { getContainingView } from '@jbrowse/core/util' import { bpSpanPx } from '@jbrowse/core/util' import { observer } from 'mobx-react' -import { isStateTreeNode } from 'mobx-state-tree' -import type { - BaseLinearDisplayModel, - LinearGenomeViewModel, -} from '@jbrowse/plugin-linear-genome-view' -import { postDraw } from '../CanvasFeatureRenderer' +import type { BaseLinearDisplayModel } from '@jbrowse/plugin-linear-genome-view' -// locals -import BoxGlyph from '../FeatureGlyphs/Box' -import GeneGlyph from '../FeatureGlyphs/Gene' import { LaidOutFeatureRect } from '../FeatureGlyph' // used so that user can click-away-from-feature below the laid out features @@ -38,52 +29,17 @@ function CanvasRendering(props: { height, regions, bpPerPx, - layoutRecords, } = props const { selectedFeatureId, featureIdUnderMouse, contextMenuFeature } = displayModel || {} - const view = isStateTreeNode(displayModel) - ? (getContainingView(displayModel) as LinearGenomeViewModel) - : undefined - - const { dynamicBlocks, staticBlocks, offsetPx: viewOffsetPx = 0 } = view || {} - const { offsetPx: blockOffsetPx = 0 } = staticBlocks?.contentBlocks[0] || {} - const { start: viewStart } = dynamicBlocks?.contentBlocks[0] || {} - const offsetPx = viewOffsetPx - blockOffsetPx const region = regions[0]! const highlightOverlayCanvas = useRef(null) - const labelsCanvas = useRef(null) const [mouseIsDown, setMouseIsDown] = useState(false) const [movedDuringLastMouseDown, setMovedDuringLastMouseDown] = useState(false) - useEffect(() => { - const canvas = labelsCanvas.current - if (!canvas) { - return - } - const ctx = canvas.getContext('2d') - if (!ctx) { - return - } - - if (viewStart === undefined) { - return - } - ctx.clearRect(0, 0, canvas.width, canvas.height) - postDraw({ - ctx, - layoutRecords: layoutRecords.map(rec => { - const glyph = rec.f.type === 'gene' ? new GeneGlyph() : new BoxGlyph() - return { ...rec, glyph } - }), - offsetPx, - regions: [{ start: viewStart }], - }) - }, [layoutRecords, viewStart, offsetPx]) - useEffect(() => { const canvas = highlightOverlayCanvas.current if (!canvas) { @@ -264,18 +220,6 @@ function CanvasRendering(props: { onFocus={() => {}} onBlur={() => {}} /> - ) } diff --git a/plugins/linear-genome-view/src/BaseLinearDisplay/components/BaseLinearDisplay.tsx b/plugins/linear-genome-view/src/BaseLinearDisplay/components/BaseLinearDisplay.tsx index cbbb0cb149..adb9657995 100644 --- a/plugins/linear-genome-view/src/BaseLinearDisplay/components/BaseLinearDisplay.tsx +++ b/plugins/linear-genome-view/src/BaseLinearDisplay/components/BaseLinearDisplay.tsx @@ -9,6 +9,8 @@ import { Menu } from '@jbrowse/core/ui' import LinearBlocks from './LinearBlocks' import { BaseLinearDisplayModel } from '../models/BaseLinearDisplayModel' +import { clamp, getContainingView } from '@jbrowse/core/util' +import { LinearGenomeViewModel } from '../../LinearGenomeView' const useStyles = makeStyles()({ display: { @@ -22,12 +24,43 @@ const useStyles = makeStyles()({ type Coord = [number, number] +const FloatingLabels = observer(function ({ + model, +}: { + model: BaseLinearDisplayModel +}) { + const view = getContainingView(model) as LinearGenomeViewModel + const { bpPerPx, offsetPx } = view + return ( +
+ {[...model.layoutFeatures.entries()].map(([key, val]) => + val ? ( +
+ {key} +
+ ) : null, + )} +
+ ) +}) + const BaseLinearDisplay = observer(function (props: { model: BaseLinearDisplayModel children?: React.ReactNode }) { const { classes } = useStyles() - const theme = useTheme() const ref = useRef(null) const [clientRect, setClientRect] = useState() const [offsetMouseCoord, setOffsetMouseCoord] = useState([0, 0]) @@ -35,7 +68,6 @@ const BaseLinearDisplay = observer(function (props: { const [contextCoord, setContextCoord] = useState() const { model, children } = props const { TooltipComponent, DisplayMessageComponent, height } = model - const items = model.contextMenuItems() return (
)} {children} + - - 0} - onMenuItemClick={(_, callback) => { - callback() - setContextCoord(undefined) - }} - onClose={() => { - setContextCoord(undefined) - model.setContextMenuFeature(undefined) - }} - TransitionProps={{ - onExit: () => { - setContextCoord(undefined) - model.setContextMenuFeature(undefined) - }, - }} - anchorReference="anchorPosition" - anchorPosition={ - contextCoord - ? { top: contextCoord[1], left: contextCoord[0] } - : undefined - } - style={{ - zIndex: theme.zIndex.tooltip, - }} - menuItems={items} - /> + {contextCoord ? ( + setContextCoord(undefined)} + /> + ) : null}
) }) +function MenuPage({ + onClose, + contextCoord, + model, +}: { + model: BaseLinearDisplayModel + contextCoord: Coord + onClose: () => void +}) { + const items = model.contextMenuItems() + const theme = useTheme() + return ( + 0} + onMenuItemClick={(_, callback) => { + callback() + onClose() + }} + onClose={() => { + onClose() + model.setContextMenuFeature(undefined) + }} + TransitionProps={{ + onExit: () => { + onClose() + model.setContextMenuFeature(undefined) + }, + }} + anchorReference="anchorPosition" + anchorPosition={ + contextCoord + ? { top: contextCoord[1], left: contextCoord[0] } + : undefined + } + style={{ + zIndex: theme.zIndex.tooltip, + }} + menuItems={items} + /> + ) +} + export default BaseLinearDisplay export { default as Tooltip } from './Tooltip' diff --git a/plugins/linear-genome-view/src/BaseLinearDisplay/models/BaseLinearDisplayModel.tsx b/plugins/linear-genome-view/src/BaseLinearDisplay/models/BaseLinearDisplayModel.tsx index 8429d5aee1..6c79885426 100644 --- a/plugins/linear-genome-view/src/BaseLinearDisplay/models/BaseLinearDisplayModel.tsx +++ b/plugins/linear-genome-view/src/BaseLinearDisplay/models/BaseLinearDisplayModel.tsx @@ -147,8 +147,8 @@ function stateModelFactory() { .views(self => ({ /** * #getter - * a CompositeMap of `featureId -> feature obj` that - * just looks in all the block data for that feature + * a CompositeMap of `featureId -> feature obj` that just looks in all + * the block data for that feature */ get features() { const featureMaps = [] @@ -168,6 +168,19 @@ function stateModelFactory() { return feat ? this.features.get(feat) : undefined }, + /** + * #getter + */ + get layoutFeatures() { + const featureMaps = [] + for (const block of self.blockState.values()) { + if (block.layout) { + featureMaps.push(block.layout.rectangles) + } + } + return new CompositeMap(featureMaps) + }, + /** * #getter */ From 7024825188cee283b77f3ec90735a2b180aa8a67 Mon Sep 17 00:00:00 2001 From: Colin Date: Tue, 24 Sep 2024 21:30:48 -0400 Subject: [PATCH 24/28] Remove layoutRecords concept --- .../core/util/layouts/GranularRectLayout.ts | 6 +-- .../CanvasFeatureRenderer.ts | 43 ------------------- .../src/CanvasFeatureRenderer/FeatureGlyph.ts | 13 +++--- .../components/BaseLinearDisplay.tsx | 14 +++--- 4 files changed, 16 insertions(+), 60 deletions(-) diff --git a/packages/core/util/layouts/GranularRectLayout.ts b/packages/core/util/layouts/GranularRectLayout.ts index 49b43d5353..6bfd961b9b 100644 --- a/packages/core/util/layouts/GranularRectLayout.ts +++ b/packages/core/util/layouts/GranularRectLayout.ts @@ -507,10 +507,10 @@ export default class GranularRectLayout implements BaseLayout { getRectangles(): Map { return new Map( [...this.rectangles.entries()].map(([id, rect]) => { - const { l, r, originalHeight, top } = rect + const { l, r, originalHeight, top, data } = rect const t = (top || 0) * this.pitchY const b = t + originalHeight - return [id, [l * this.pitchX, t, r * this.pitchX, b]] // left, top, right, bottom + return [id, [l * this.pitchX, t, r * this.pitchX, b, data]] // left, top, right, bottom }), ) } @@ -531,7 +531,7 @@ export default class GranularRectLayout implements BaseLayout { const x2 = region.end // add +/- pitchX to avoid resolution causing errors if (segmentsIntersect(x1, x2, y1 - this.pitchX, y2 + this.pitchX)) { - regionRectangles[id] = [y1, t, y2, b] + regionRectangles[id] = [y1, t, y2, b, rect.data] } } } diff --git a/plugins/canvas/src/CanvasFeatureRenderer/CanvasFeatureRenderer.ts b/plugins/canvas/src/CanvasFeatureRenderer/CanvasFeatureRenderer.ts index 48f948034d..2822952b41 100644 --- a/plugins/canvas/src/CanvasFeatureRenderer/CanvasFeatureRenderer.ts +++ b/plugins/canvas/src/CanvasFeatureRenderer/CanvasFeatureRenderer.ts @@ -77,14 +77,6 @@ export default class CanvasRenderer extends BoxRendererType { this.drawRect(ctx, fRect, props) } - if (props.exportSVG) { - postDraw({ - ctx, - layoutRecords: layoutRecords, - offsetPx: 0, - ...props, - }) - } return undefined } @@ -127,17 +119,6 @@ export default class CanvasRenderer extends BoxRendererType { return { ...results, ...res, - layoutRecords: layoutRecords.map(rec => ({ - label: rec.label, - description: rec.description, - l: rec.l, - t: rec.t, - f: { - start: rec.f.get('start'), - end: rec.f.get('end'), - type: rec.f.get('type'), - }, - })), features, layout, height, @@ -147,30 +128,6 @@ export default class CanvasRenderer extends BoxRendererType { } } -export function postDraw({ - ctx, - layoutRecords, - offsetPx, - regions, -}: { - ctx: CanvasRenderingContext2D - regions: { - start: number - }[] - offsetPx: number - layoutRecords: PostDrawFeatureRectWithGlyph[] -}) { - ctx.fillStyle = 'black' - ctx.font = '10px sans-serif' - layoutRecords.filter(notEmpty).forEach(record => { - record.glyph.postDraw(ctx, { - record, - regions, - offsetPx, - }) - }) -} - export { type RenderArgs, type RenderArgsSerialized, diff --git a/plugins/canvas/src/CanvasFeatureRenderer/FeatureGlyph.ts b/plugins/canvas/src/CanvasFeatureRenderer/FeatureGlyph.ts index 1c9af81e2d..ae7ac2b19b 100644 --- a/plugins/canvas/src/CanvasFeatureRenderer/FeatureGlyph.ts +++ b/plugins/canvas/src/CanvasFeatureRenderer/FeatureGlyph.ts @@ -59,7 +59,9 @@ export default abstract class FeatureGlyph { const leftBase = region.start const startbp = fRect.l / scale + leftBase const endbp = (fRect.l + fRect.w) / scale + leftBase - const top = layout.addRect(feature.id(), startbp, endbp, fRect.h) + const top = layout.addRect(feature.id(), startbp, endbp, fRect.h, { + label: feature.get('name') || feature.get('id'), + }) return top === null ? null @@ -78,12 +80,9 @@ export default abstract class FeatureGlyph { getFeatureRectangle(viewInfo: ViewInfo, feature: Feature) { const { region, bpPerPx } = viewInfo - const [leftPx, rightPx] = bpSpanPx( - feature.get('start'), - feature.get('end'), - region, - bpPerPx, - ) + const s = feature.get('start') + const e = feature.get('end') + const [leftPx, rightPx] = bpSpanPx(s, e, region, bpPerPx) return { l: leftPx, diff --git a/plugins/linear-genome-view/src/BaseLinearDisplay/components/BaseLinearDisplay.tsx b/plugins/linear-genome-view/src/BaseLinearDisplay/components/BaseLinearDisplay.tsx index adb9657995..bb1e131284 100644 --- a/plugins/linear-genome-view/src/BaseLinearDisplay/components/BaseLinearDisplay.tsx +++ b/plugins/linear-genome-view/src/BaseLinearDisplay/components/BaseLinearDisplay.tsx @@ -9,7 +9,7 @@ import { Menu } from '@jbrowse/core/ui' import LinearBlocks from './LinearBlocks' import { BaseLinearDisplayModel } from '../models/BaseLinearDisplayModel' -import { clamp, getContainingView } from '@jbrowse/core/util' +import { clamp, getContainingView, measureText } from '@jbrowse/core/util' import { LinearGenomeViewModel } from '../../LinearGenomeView' const useStyles = makeStyles()({ @@ -33,8 +33,8 @@ const FloatingLabels = observer(function ({ const { bpPerPx, offsetPx } = view return (
- {[...model.layoutFeatures.entries()].map(([key, val]) => - val ? ( + {[...model.layoutFeatures.entries()].map(([key, val]) => { + return val ? (
- {key} + {val[4].label}
- ) : null, - )} + ) : null + })}
) }) From e0291c933d832f31b8298bf18d95db429614d8e8 Mon Sep 17 00:00:00 2001 From: Colin Date: Tue, 24 Sep 2024 22:33:00 -0400 Subject: [PATCH 25/28] Updates --- .../core/util/layouts/GranularRectLayout.ts | 2 ++ .../FeatureGlyphs/Box.ts | 32 ------------------- .../FeatureGlyphs/Gene.ts | 1 + .../FeatureGlyphs/ProcessedTranscript.ts | 8 ++--- .../components/CanvasFeatureRendering.tsx | 10 +++--- .../components/BaseLinearDisplay.tsx | 16 ++++++---- 6 files changed, 21 insertions(+), 48 deletions(-) diff --git a/packages/core/util/layouts/GranularRectLayout.ts b/packages/core/util/layouts/GranularRectLayout.ts index 6bfd961b9b..d8181d2aa9 100644 --- a/packages/core/util/layouts/GranularRectLayout.ts +++ b/packages/core/util/layouts/GranularRectLayout.ts @@ -505,6 +505,7 @@ export default class GranularRectLayout implements BaseLayout { } getRectangles(): Map { + // @ts-expect-error return new Map( [...this.rectangles.entries()].map(([id, rect]) => { const { l, r, originalHeight, top, data } = rect @@ -531,6 +532,7 @@ export default class GranularRectLayout implements BaseLayout { const x2 = region.end // add +/- pitchX to avoid resolution causing errors if (segmentsIntersect(x1, x2, y1 - this.pitchX, y2 + this.pitchX)) { + // @ts-expect-error regionRectangles[id] = [y1, t, y2, b, rect.data] } } diff --git a/plugins/canvas/src/CanvasFeatureRenderer/FeatureGlyphs/Box.ts b/plugins/canvas/src/CanvasFeatureRenderer/FeatureGlyphs/Box.ts index a1d97a7dfc..0719a4d530 100644 --- a/plugins/canvas/src/CanvasFeatureRenderer/FeatureGlyphs/Box.ts +++ b/plugins/canvas/src/CanvasFeatureRenderer/FeatureGlyphs/Box.ts @@ -151,36 +151,4 @@ export default class Box extends FeatureGlyph { context.fillStyle = readConfObject(config, 'color1', { feature }) context.fillRect(left, top, Math.max(1, width), height) } - - postDraw( - ctx: CanvasRenderingContext2D, - props: { - record: LaidOutFeatureRect - regions: { - start: number - }[] - offsetPx: number - }, - ) { - const { regions, record, offsetPx } = props - const { f, l, t, label, description } = record - const region = regions[0]! - - function renderText({ text, offsetY }: { text: string; offsetY: number }) { - if (f.start < region.start && region.start < f.end) { - ctx.fillText(text, offsetPx, t + offsetY) - } else { - ctx.fillText(text, l, t + offsetY) - } - } - if (label) { - ctx.fillStyle = 'black' - renderText(label) - } - - if (description) { - ctx.fillStyle = 'blue' - renderText(description) - } - } } diff --git a/plugins/canvas/src/CanvasFeatureRenderer/FeatureGlyphs/Gene.ts b/plugins/canvas/src/CanvasFeatureRenderer/FeatureGlyphs/Gene.ts index d0d4e5c950..ae0d360da1 100644 --- a/plugins/canvas/src/CanvasFeatureRenderer/FeatureGlyphs/Gene.ts +++ b/plugins/canvas/src/CanvasFeatureRenderer/FeatureGlyphs/Gene.ts @@ -38,6 +38,7 @@ export default class Gene extends BoxGlyph { const glyph = this.getSubGlyph(sub) const subRect = glyph.getFeatureRectangle(subArgs, sub) const rect = subRect.rect + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition if (!rect) { console.warn('feature not laid out') return diff --git a/plugins/canvas/src/CanvasFeatureRenderer/FeatureGlyphs/ProcessedTranscript.ts b/plugins/canvas/src/CanvasFeatureRenderer/FeatureGlyphs/ProcessedTranscript.ts index 17ef5a4f55..d42c3bf105 100644 --- a/plugins/canvas/src/CanvasFeatureRenderer/FeatureGlyphs/ProcessedTranscript.ts +++ b/plugins/canvas/src/CanvasFeatureRenderer/FeatureGlyphs/ProcessedTranscript.ts @@ -135,14 +135,13 @@ export default class ProcessedTranscript extends SegmentsGlyph { let codeStart = Number.POSITIVE_INFINITY let codeEnd = Number.NEGATIVE_INFINITY - let haveLeftUTR - let haveRightUTR + let haveLeftUTR: boolean | undefined + let haveRightUTR: boolean | undefined // gather exons, find coding start and end, and look for UTRs - let type const exons = [] as Feature[] subparts.forEach(sub => { - type = sub.get('type') + const type = sub.get('type') if (/^cds/i.test(type)) { if (codeStart > sub.get('start')) { codeStart = sub.get('start') @@ -178,6 +177,7 @@ export default class ProcessedTranscript extends SegmentsGlyph { let start: number let end: number if (!haveLeftUTR) { + // eslint-disable-next-line @typescript-eslint/prefer-for-of for (let i = 0; i < exons.length; i++) { const exon = exons[i]! start = exon.get('start') diff --git a/plugins/canvas/src/CanvasFeatureRenderer/components/CanvasFeatureRendering.tsx b/plugins/canvas/src/CanvasFeatureRenderer/components/CanvasFeatureRendering.tsx index 19f5b58cfa..a48235ec04 100644 --- a/plugins/canvas/src/CanvasFeatureRenderer/components/CanvasFeatureRendering.tsx +++ b/plugins/canvas/src/CanvasFeatureRenderer/components/CanvasFeatureRendering.tsx @@ -11,7 +11,7 @@ import { LaidOutFeatureRect } from '../FeatureGlyph' // (issue #1248) const canvasPadding = 100 -function CanvasRendering(props: { +const CanvasFeatureRendering = observer(function (props: { blockKey: string displayModel?: BaseLinearDisplayModel width: number @@ -51,7 +51,7 @@ function CanvasRendering(props: { } ctx.clearRect(0, 0, canvas.width, canvas.height) const selectedRect = selectedFeatureId - ? displayModel?.getFeatureByID?.(blockKey, selectedFeatureId) + ? displayModel?.getFeatureByID(blockKey, selectedFeatureId) : undefined if (selectedRect) { const [leftBp, topPx, rightBp, bottomPx] = selectedRect @@ -73,7 +73,7 @@ function CanvasRendering(props: { } const highlightedFeature = featureIdUnderMouse || contextMenuFeature?.id() const highlightedRect = highlightedFeature - ? displayModel?.getFeatureByID?.(blockKey, highlightedFeature) + ? displayModel?.getFeatureByID(blockKey, highlightedFeature) : undefined if (highlightedRect) { const [leftBp, topPx, rightBp, bottomPx] = highlightedRect @@ -222,6 +222,6 @@ function CanvasRendering(props: { /> ) -} +}) -export default observer(CanvasRendering) +export default CanvasFeatureRendering diff --git a/plugins/linear-genome-view/src/BaseLinearDisplay/components/BaseLinearDisplay.tsx b/plugins/linear-genome-view/src/BaseLinearDisplay/components/BaseLinearDisplay.tsx index 6e4a05ce2c..af1d10ec5c 100644 --- a/plugins/linear-genome-view/src/BaseLinearDisplay/components/BaseLinearDisplay.tsx +++ b/plugins/linear-genome-view/src/BaseLinearDisplay/components/BaseLinearDisplay.tsx @@ -44,7 +44,8 @@ const FloatingLabels = observer(function ({ {[...model.layoutFeatures.entries()] .filter(f => !!f[1]) .map(([key, val]) => { - const [left, top, right, bottom, feature] = val! + // @ts-expect-error + const [left, , right, bottom, feature] = val! const { refName, label } = feature! const r0 = assembly.getCanonicalRefName(refName) || refName const r = view.bpToPx({ @@ -138,7 +139,9 @@ const BaseLinearDisplay = observer(function (props: { setContextCoord(undefined)} + onClose={() => { + setContextCoord(undefined) + }} /> ) : null} @@ -174,11 +177,10 @@ function MenuPage({ }, }} anchorReference="anchorPosition" - anchorPosition={ - contextCoord - ? { top: contextCoord[1], left: contextCoord[0] } - : undefined - } + anchorPosition={{ + top: contextCoord[1], + left: contextCoord[0], + }} style={{ zIndex: theme.zIndex.tooltip, }} From 2330fdb0a9b048f5d6744d836f3a41272e418fa9 Mon Sep 17 00:00:00 2001 From: Colin Date: Sun, 17 Nov 2024 13:03:04 -0500 Subject: [PATCH 26/28] Modularize --- .../components/BaseLinearDisplay.tsx | 59 +---------------- .../components/FloatingLabels.tsx | 63 +++++++++++++++++++ 2 files changed, 64 insertions(+), 58 deletions(-) create mode 100644 plugins/linear-genome-view/src/BaseLinearDisplay/components/FloatingLabels.tsx diff --git a/plugins/linear-genome-view/src/BaseLinearDisplay/components/BaseLinearDisplay.tsx b/plugins/linear-genome-view/src/BaseLinearDisplay/components/BaseLinearDisplay.tsx index af1d10ec5c..cfce01b61f 100644 --- a/plugins/linear-genome-view/src/BaseLinearDisplay/components/BaseLinearDisplay.tsx +++ b/plugins/linear-genome-view/src/BaseLinearDisplay/components/BaseLinearDisplay.tsx @@ -9,13 +9,7 @@ import { Menu } from '@jbrowse/core/ui' import LinearBlocks from './LinearBlocks' import { BaseLinearDisplayModel } from '../models/BaseLinearDisplayModel' -import { - clamp, - getContainingView, - getSession, - measureText, -} from '@jbrowse/core/util' -import { LinearGenomeViewModel } from '../../LinearGenomeView' +import FloatingLabels from './FloatingLabels' const useStyles = makeStyles()({ display: { @@ -29,57 +23,6 @@ const useStyles = makeStyles()({ type Coord = [number, number] -const FloatingLabels = observer(function ({ - model, -}: { - model: BaseLinearDisplayModel -}) { - const view = getContainingView(model) as LinearGenomeViewModel - const { assemblyManager } = getSession(model) - const { offsetPx } = view - const assemblyName = view.assemblyNames[0] - const assembly = assemblyName ? assemblyManager.get(assemblyName) : undefined - return assembly ? ( -
- {[...model.layoutFeatures.entries()] - .filter(f => !!f[1]) - .map(([key, val]) => { - // @ts-expect-error - const [left, , right, bottom, feature] = val! - const { refName, label } = feature! - const r0 = assembly.getCanonicalRefName(refName) || refName - const r = view.bpToPx({ - refName: r0, - coord: left, - })?.offsetPx - const r2 = view.bpToPx({ - refName: r0, - coord: right, - })?.offsetPx - return r !== undefined ? ( -
- {label} -
- ) : null - })} -
- ) : null -}) - const BaseLinearDisplay = observer(function (props: { model: BaseLinearDisplayModel children?: React.ReactNode diff --git a/plugins/linear-genome-view/src/BaseLinearDisplay/components/FloatingLabels.tsx b/plugins/linear-genome-view/src/BaseLinearDisplay/components/FloatingLabels.tsx new file mode 100644 index 0000000000..17045d0346 --- /dev/null +++ b/plugins/linear-genome-view/src/BaseLinearDisplay/components/FloatingLabels.tsx @@ -0,0 +1,63 @@ +import React from 'react' +import { observer } from 'mobx-react' + +// locals +import { BaseLinearDisplayModel } from '../models/BaseLinearDisplayModel' +import { + clamp, + getContainingView, + getSession, + measureText, +} from '@jbrowse/core/util' +import { LinearGenomeViewModel } from '../../LinearGenomeView' + +const FloatingLabels = observer(function ({ + model, +}: { + model: BaseLinearDisplayModel +}) { + const view = getContainingView(model) as LinearGenomeViewModel + const { assemblyManager } = getSession(model) + const { offsetPx } = view + const assemblyName = view.assemblyNames[0] + const assembly = assemblyName ? assemblyManager.get(assemblyName) : undefined + + return assembly ? ( + + {[...model.layoutFeatures.entries()] + .filter(f => !!f[1]) + .map(([key, val]) => { + // @ts-expect-error + const [left, , right, bottom, feature] = val! + const { refName, label } = feature! + const r0 = assembly.getCanonicalRefName(refName) || refName + const r = view.bpToPx({ + refName: r0, + coord: left, + })?.offsetPx + const r2 = view.bpToPx({ + refName: r0, + coord: right, + })?.offsetPx + return r !== undefined ? ( + + {label} + + ) : null + })} + + ) : null +}) + +export default FloatingLabels From 15883e9c728cd2cd4fe5f2b286d396356cc4ba63 Mon Sep 17 00:00:00 2001 From: Colin Date: Sun, 17 Nov 2024 14:37:29 -0500 Subject: [PATCH 27/28] Misc --- .../FeatureGlyphs/Box.ts | 38 +- .../FeatureGlyphs/ProcessedTranscript.ts | 433 +++++++++--------- .../components/FloatingLabels.tsx | 36 +- 3 files changed, 271 insertions(+), 236 deletions(-) diff --git a/plugins/canvas/src/CanvasFeatureRenderer/FeatureGlyphs/Box.ts b/plugins/canvas/src/CanvasFeatureRenderer/FeatureGlyphs/Box.ts index 0719a4d530..9942204e72 100644 --- a/plugins/canvas/src/CanvasFeatureRenderer/FeatureGlyphs/Box.ts +++ b/plugins/canvas/src/CanvasFeatureRenderer/FeatureGlyphs/Box.ts @@ -72,12 +72,17 @@ export default class Box extends FeatureGlyph { return this.expandRectangleWithLabels(viewArgs, feature, { ...fRect, w, - rect: { l, h, w, t: 0 }, + rect: { + l, + h, + w, + t: 0, + }, }) } // given an under-construction feature layout rectangle, expand it to - // accommodate a label and/or a description + // accommodate label and/or a description expandRectangleWithLabels( view: ViewInfo, f: Feature, @@ -86,7 +91,7 @@ export default class Box extends FeatureGlyph { const { config } = view const showLabels = readConfObject(config, 'showLabels') const label = showLabels ? this.makeFeatureLabel(f, fRect) : undefined - if (label) { + if (label?.text) { fRect.h += label.h fRect.w = Math.max(label.w, fRect.w) label.offsetY = fRect.h @@ -95,12 +100,16 @@ export default class Box extends FeatureGlyph { const description = showDescriptions ? this.makeFeatureDescriptionLabel(f, fRect) : undefined - if (description) { + if (description?.text) { fRect.h += description.h fRect.w = Math.max(description.w, fRect.w) description.offsetY = fRect.h // -marginBottom removed in jb2 } - return { ...fRect, description, label } + return { + ...fRect, + description, + label, + } } _embeddedImages = { @@ -141,14 +150,27 @@ export default class Box extends FeatureGlyph { bpPerPx, ) const left = leftPx - const width = rightPx - leftPx + const width = Math.max(rightPx - leftPx, 2) const height = this.getFeatureHeight(viewInfo, feature) if (height !== overallHeight) { top += (overallHeight - height) / 2 } - context.fillStyle = readConfObject(config, 'color1', { feature }) - context.fillRect(left, top, Math.max(1, width), height) + context.fillStyle = this.isUTR(feature) + ? readConfObject(config, 'color3', { feature }) + : readConfObject(config, 'color1', { feature }) + context.fillRect(left, top, width, height) + } + + getFeatureHeight(viewInfo: ViewInfo, feature: Feature) { + const height = super.getFeatureHeight(viewInfo, feature) + return this.isUTR(feature) ? height * 0.2 : height + } + + protected isUTR(feature: Feature) { + return /(\bUTR|_UTR|untranslated[_\s]region)\b/.test( + feature.get('type') || '', + ) } } diff --git a/plugins/canvas/src/CanvasFeatureRenderer/FeatureGlyphs/ProcessedTranscript.ts b/plugins/canvas/src/CanvasFeatureRenderer/FeatureGlyphs/ProcessedTranscript.ts index d42c3bf105..9bf6c9db5f 100644 --- a/plugins/canvas/src/CanvasFeatureRenderer/FeatureGlyphs/ProcessedTranscript.ts +++ b/plugins/canvas/src/CanvasFeatureRenderer/FeatureGlyphs/ProcessedTranscript.ts @@ -1,247 +1,256 @@ -import SegmentsGlyph from './Segments' import { SimpleFeature, Feature } from '@jbrowse/core/util' -import { ViewInfo } from '../FeatureGlyph' -export default class ProcessedTranscript extends SegmentsGlyph { - protected getSubparts(f: Feature) { - const c = f.children() - if (!c) { - return [] - } +import SegmentsGlyph from './Segments' +import { + AnyConfigurationModel, + readConfObject, +} from '@jbrowse/core/configuration' +import { LaidOutFeatureRect, ViewInfo } from '../FeatureGlyph' - // if (c && this.config.inferCdsParts) { - // c = this.makeCDSs(f, c) - // } +function isUTR(feature: Feature) { + return /(\bUTR|_UTR|untranslated[_\s]region)\b/.test( + feature.get('type') || '', + ) +} - // if (c && this.config.impliedUTRs) { - // c = this.makeUTRs(f, c) - // } +// returns a callback that will filter features features according to the +// subParts conf var +function makeSubpartsFilter(confKey: string | string[]) { + const ret = ['CDS', 'UTR', 'five_prime_UTR', 'three_prime_UTR'] - return c - } + return (feature: Feature) => + ret + .map(typeName => typeName.toLowerCase()) + .includes(feature.get('type').toLowerCase()) +} + +function filterSubpart(feature: Feature) { + return makeSubpartsFilter('subParts')(feature) +} + +function makeCDSs(parent: Feature, subparts: Feature[]) { + // infer CDS parts from exon coordinates + + let codeStart = Number.POSITIVE_INFINITY + let codeEnd = Number.NEGATIVE_INFINITY - protected makeCDSs(parent: Feature, subparts: Feature[]) { - // infer CDS parts from exon coordinates - - let codeStart = Number.POSITIVE_INFINITY - let codeEnd = Number.NEGATIVE_INFINITY - - // gather exons, find coding start and end - let type: string - const codeIndices = [] - const exons = [] - for (let i = 0; i < subparts.length; i++) { - const subpart = subparts[i]! - type = subpart.get('type') - if (/^cds/i.test(type)) { - // if any CDSs parts are present already, - // bail and return all subparts as-is - if (/:CDS:/i.test(subpart.get('name'))) { - return subparts - } - - codeIndices.push(i) - if (codeStart > subpart.get('start')) { - codeStart = subpart.get('start') - } - if (codeEnd < subpart.get('end')) { - codeEnd = subpart.get('end') - } - } else { - if (/exon/i.test(type)) { - exons.push(subpart) - } + // gather exons, find coding start and end + let type: string + const codeIndices = [] + const exons = [] + for (let i = 0; i < subparts.length; i++) { + const subpart = subparts[i]! + type = subpart.get('type') + if (/^cds/i.test(type)) { + // if any CDSs parts are present already, + // bail and return all subparts as-is + if (/:CDS:/i.test(subpart.get('name'))) { + return subparts } - } - // splice out unspliced cds parts - codeIndices.sort((a, b) => { - return b - a - }) - for (let i = codeIndices.length - 1; i >= 0; i--) { - subparts.splice(codeIndices[i]!, 1) + codeIndices.push(i) + if (codeStart > subpart.get('start')) { + codeStart = subpart.get('start') + } + if (codeEnd < subpart.get('end')) { + codeEnd = subpart.get('end') + } + } else { + if (/exon/i.test(type)) { + exons.push(subpart) + } } + } - // bail if we don't have exons and cds - if ( - !( - exons.length && - codeStart < Number.POSITIVE_INFINITY && - codeEnd > Number.NEGATIVE_INFINITY - ) - ) { - return subparts + // splice out unspliced cds parts + codeIndices.sort((a, b) => b - a) + for (let i = codeIndices.length - 1; i >= 0; i--) { + subparts.splice(codeIndices[i]!, 1) + } + + // bail if we don't have exons and cds + if ( + !( + exons.length && + codeStart < Number.POSITIVE_INFINITY && + codeEnd > Number.NEGATIVE_INFINITY + ) + ) { + return subparts + } + + // make sure the exons are sorted by coord + exons.sort((a, b) => a.get('start') - b.get('start')) + + // iterate thru exons again, and calculate cds parts + const strand = parent.get('strand') + let codePartStart = Number.POSITIVE_INFINITY + let codePartEnd = Number.NEGATIVE_INFINITY + for (let i = 0; i < exons.length; i++) { + const start = exons[i]!.get('start') + const end = exons[i]!.get('end') + + // CDS containing exon + if (codeStart >= start && codeEnd <= end) { + codePartStart = codeStart + codePartEnd = codeEnd + } + // 5' terminal CDS part + else if (codeStart >= start && codeStart < end) { + codePartStart = codeStart + codePartEnd = end + } + // 3' terminal CDS part + else if (codeEnd > start && codeEnd <= end) { + codePartStart = start + codePartEnd = codeEnd + } + // internal CDS part + else if (start < codeEnd && end > codeStart) { + codePartStart = start + codePartEnd = end } - // make sure the exons are sorted by coord - exons.sort((a, b) => a.get('start') - b.get('start')) + // "splice in" the calculated cds part into subparts at beginning of + // _makeCDSs() method, bail if cds subparts are encountered + subparts.splice( + i, + 0, + new SimpleFeature({ + id: `${parent.get('uniqueID')}:CDS:${i}`, + data: { + parent: parent, + start: codePartStart, + end: codePartEnd, + strand: strand, + type: 'CDS', + name: `${parent.get('uniqueID')}:CDS:${i}`, + }, + }), + ) + } - // iterate thru exons again, and calculate cds parts - const strand = parent.get('strand') - let codePartStart = Number.POSITIVE_INFINITY - let codePartEnd = Number.NEGATIVE_INFINITY - for (let i = 0; i < exons.length; i++) { - const start = exons[i]!.get('start') - const end = exons[i]!.get('end') + // make sure the subparts are sorted by coord + return subparts.sort((a, b) => a.get('start') - b.get('start')) +} - // CDS containing exon - if (codeStart >= start && codeEnd <= end) { - codePartStart = codeStart - codePartEnd = codeEnd - } - // 5' terminal CDS part - else if (codeStart >= start && codeStart < end) { - codePartStart = codeStart - codePartEnd = end - } - // 3' terminal CDS part - else if (codeEnd > start && codeEnd <= end) { - codePartStart = start - codePartEnd = codeEnd +function makeUTRs(parent: Feature, subs: Feature[]) { + // based on Lincoln's UTR-making code in + // Bio::Graphics::Glyph::processed_transcript + const subparts = [...subs] + + let codeStart = Number.POSITIVE_INFINITY + let codeEnd = Number.NEGATIVE_INFINITY + + let haveLeftUTR: boolean | undefined + let haveRightUTR: boolean | undefined + + // gather exons, find coding start and end, and look for UTRs + const exons = [] + for (const subpart of subparts) { + const type = subpart.get('type') + if (/^cds/i.test(type)) { + if (codeStart > subpart.get('start')) { + codeStart = subpart.get('start') } - // internal CDS part - else if (start < codeEnd && end > codeStart) { - codePartStart = start - codePartEnd = end + if (codeEnd < subpart.get('end')) { + codeEnd = subpart.get('end') } + } else if (/exon/i.test(type)) { + exons.push(subpart) + } else if (isUTR(subpart)) { + haveLeftUTR = subpart.get('start') === parent.get('start') + haveRightUTR = subpart.get('end') === parent.get('end') + } + } + + // bail if we don't have exons and CDS + if ( + !( + exons.length && + codeStart < Number.POSITIVE_INFINITY && + codeEnd > Number.NEGATIVE_INFINITY + ) + ) { + return subparts + } + + // make sure the exons are sorted by coord + exons.sort((a, b) => a.get('start') - b.get('start')) - // "splice in" the calculated cds part into subparts - // at beginning of _makeCDSs() method, bail if cds subparts are encountered - subparts.splice( - i, - 0, + const strand = parent.get('strand') + + // make the left-hand UTRs + let start: number | undefined + let end: number | undefined + if (!haveLeftUTR) { + for (let i = 0; i < exons.length; i++) { + start = exons[i]!.get('start') + if (start >= codeStart) { + break + } + end = Math.min(codeStart, exons[i]!.get('end')) + const type = strand >= 0 ? 'five_prime_UTR' : 'three_prime_UTR' + subparts.unshift( new SimpleFeature({ - id: `${parent.get('uniqueID')}:CDS:${i}`, - data: { - parent: parent, - start: codePartStart, - end: codePartEnd, - strand: strand, - type: 'CDS', - name: `${parent.get('uniqueID')}:CDS:${i}`, - }, + parent, + id: `${parent.id()}_${type}_${i}`, + data: { start, end, strand, type }, }), ) } - - // make sure the subparts are sorted by coord - return subparts.sort((a, b) => a.get('start') - b.get('start')) } - protected makeUTRs(parent: Feature, subparts: Feature[]) { - // based on Lincoln's UTR-making code in - // Bio::Graphics::Glyph::processed_transcript - - let codeStart = Number.POSITIVE_INFINITY - let codeEnd = Number.NEGATIVE_INFINITY - - let haveLeftUTR: boolean | undefined - let haveRightUTR: boolean | undefined - - // gather exons, find coding start and end, and look for UTRs - const exons = [] as Feature[] - subparts.forEach(sub => { - const type = sub.get('type') - if (/^cds/i.test(type)) { - if (codeStart > sub.get('start')) { - codeStart = sub.get('start') - } - if (codeEnd < sub.get('end')) { - codeEnd = sub.get('end') - } - } else if (/exon/i.test(type)) { - exons.push(sub) - } else if (this.isUTR(sub)) { - haveLeftUTR = sub.get('start') === parent.get('start') - haveRightUTR = sub.get('end') === parent.get('end') + // make the right-hand UTRs + if (!haveRightUTR) { + for (let i = exons.length - 1; i >= 0; i--) { + end = exons[i]!.get('end') + if (end <= codeEnd) { + break } - }) - // bail if we don't have exons and CDS - if ( - !( - exons.length && - codeStart < Number.POSITIVE_INFINITY && - codeEnd > Number.NEGATIVE_INFINITY + start = Math.max(codeEnd, exons[i]!.get('start')) + const type = strand >= 0 ? 'three_prime_UTR' : 'five_prime_UTR' + subparts.push( + new SimpleFeature({ + parent, + id: `${parent.id()}_${type}_${i}`, + data: { start, end, strand, type }, + }), ) - ) { - return subparts - } - - // make sure the exons are sorted by coord - exons.sort((a, b) => a.get('start') - b.get('start')) - - const strand = parent.get('strand') - - // make the left-hand UTRs - let start: number - let end: number - if (!haveLeftUTR) { - // eslint-disable-next-line @typescript-eslint/prefer-for-of - for (let i = 0; i < exons.length; i++) { - const exon = exons[i]! - start = exon.get('start') - if (start >= codeStart) { - break - } - end = codeStart > exon.get('end') ? exon.get('end') : codeStart - - subparts.unshift( - new SimpleFeature({ - id: 'wow', // FIXME - data: { - parent: parent, - start: start, - end: end, - strand: strand, - type: strand >= 0 ? 'five_prime_UTR' : 'three_prime_UTR', - }, - }), - ) - } } + } - // make the right-hand UTRs - if (!haveRightUTR) { - for (let i = exons.length - 1; i >= 0; i--) { - const exon = exons[i]! - end = exon.get('end') - if (end <= codeEnd) { - break - } - - start = codeEnd < exon.get('start') ? exon.get('start') : codeEnd - subparts.push( - new SimpleFeature({ - id: 'wow', // FIXME - data: { - parent: parent, - start: start, - end: end, - strand: strand, - type: strand >= 0 ? 'three_prime_UTR' : 'five_prime_UTR', - }, - }), - ) - } - } + return subparts +} - return subparts - } +export default class ProcessedTranscript extends SegmentsGlyph { + protected getSubparts(f: Feature) { + const c = f.children() + const isTranscript = ['mRNA', 'transcript'].includes(f.get('type')) - protected isUTR(feature: Feature) { - return /(\bUTR|_UTR|untranslated[_\s]region)\b/.test( - feature.get('type') || '', - ) - } + // if (c && this.config.inferCdsParts) { + // c = this.makeCDSs(f, c) + // } - getFeatureHeight(viewInfo: ViewInfo, feature: Feature) { - const height = super.getFeatureHeight(viewInfo, feature) + // if (c && this.config.impliedUTRs) { + // c = this.makeUTRs(f, c) + // } + console.log({ c }) - if (this.isUTR(feature)) { - return height * 0.6 - } + return !c ? [] : c.filter(element => filterSubpart(element)) + } - return height + renderSegments( + context: CanvasRenderingContext2D, + viewInfo: ViewInfo, + fRect: LaidOutFeatureRect, + ) { + const { t, f } = fRect + const subfeatures = this.getSubparts(f) + console.log({ subfeatures }) + subfeatures?.forEach(sub => { + this.renderSegment(context, viewInfo, sub, t, fRect.rect.h) + }) } } diff --git a/plugins/linear-genome-view/src/BaseLinearDisplay/components/FloatingLabels.tsx b/plugins/linear-genome-view/src/BaseLinearDisplay/components/FloatingLabels.tsx index 17045d0346..051e69a661 100644 --- a/plugins/linear-genome-view/src/BaseLinearDisplay/components/FloatingLabels.tsx +++ b/plugins/linear-genome-view/src/BaseLinearDisplay/components/FloatingLabels.tsx @@ -21,15 +21,14 @@ const FloatingLabels = observer(function ({ const { offsetPx } = view const assemblyName = view.assemblyNames[0] const assembly = assemblyName ? assemblyManager.get(assemblyName) : undefined - return assembly ? ( - +
{[...model.layoutFeatures.entries()] .filter(f => !!f[1]) .map(([key, val]) => { // @ts-expect-error const [left, , right, bottom, feature] = val! - const { refName, label } = feature! + const { refName, description, label } = feature! const r0 = assembly.getCanonicalRefName(refName) || refName const r = view.bpToPx({ refName: r0, @@ -40,23 +39,28 @@ const FloatingLabels = observer(function ({ coord: right, })?.offsetPx return r !== undefined ? ( - - {label} - +
{label}
+
{description}
+
) : null })} - + ) : null }) From a4bfbd7f3fa1bbe81c334d5f78ef920c0a7c7233 Mon Sep 17 00:00:00 2001 From: Colin Date: Thu, 30 Jan 2025 10:18:05 -0500 Subject: [PATCH 28/28] peerDeps --- plugins/canvas/package.json | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/plugins/canvas/package.json b/plugins/canvas/package.json index 39a4069b19..fa83651cb7 100644 --- a/plugins/canvas/package.json +++ b/plugins/canvas/package.json @@ -37,10 +37,12 @@ "useDist": "node ../../scripts/useDist.js", "useSrc": "node ../../scripts/useSrc.js" }, - "peerDependencies": { + "dependencies": { "@jbrowse/core": "^2.0.0", "mobx-react": "^9.0.0", - "mobx-state-tree": "^5.0.0", + "mobx-state-tree": "^5.0.0" + }, + "peerDependencies": { "react": ">=16.8.0" }, "publishConfig": {