Skip to content

Commit

Permalink
Merge pull request #2534 from GMOD/add-arc-plugin
Browse files Browse the repository at this point in the history
New display type for drawing arcs
  • Loading branch information
cmdcolin committed Dec 15, 2021
2 parents 50c334c + b759efa commit 3dabffa
Show file tree
Hide file tree
Showing 25 changed files with 675 additions and 0 deletions.
1 change: 1 addition & 0 deletions packages/core/util/jexl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ export default function (/* config?: any*/): JexlNonBuildable {
j.addFunction('floor', Math.floor)
j.addFunction('round', Math.round)
j.addFunction('abs', Math.abs)
j.addFunction('log10', Math.log10)
j.addFunction('parseInt', Number.parseInt)
j.addFunction('parseFloat', Number.parseFloat)

Expand Down
53 changes: 53 additions & 0 deletions plugins/arc/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
{
"name": "@jbrowse/plugin-arc",
"version": "1.5.1",
"description": "JBrowse 2 arc adapters, tracks, etc.",
"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/arc"
},
"author": "JBrowse Team",
"distMain": "dist/index.js",
"srcMain": "src/index.ts",
"main": "src/index.ts",
"distModule": "dist/plugin-arc.esm.js",
"module": "",
"files": [
"dist",
"src"
],
"scripts": {
"start": "tsdx watch --verbose --noClean",
"build": "tsdx build",
"test": "cd ../..; jest plugins/arc",
"prepublishOnly": "yarn test",
"prepack": "yarn build; yarn useDist",
"postpack": "yarn useSrc",
"useDist": "node ../../scripts/useDist.js",
"useSrc": "node ../../scripts/useSrc.js"
},
"dependencies": {
"react-svg-tooltip": "^0.0.11"
},
"peerDependencies": {
"@jbrowse/core": "^1.0.0",
"@jbrowse/plugin-linear-genome-view": "^1.0.0",
"@jbrowse/plugin-wiggle": "^1.0.0",
"@material-ui/core": "^4.12.2",
"mobx": "^5.10.1",
"mobx-react": "^6.0.0",
"mobx-state-tree": "3.14.1",
"prop-types": "^15.0.0",
"react": ">=16.8.0",
"rxjs": "^6.0.0"
},
"private": true
}
3 changes: 3 additions & 0 deletions plugins/arc/src/ArcRenderer/ArcRenderer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import FeatureRendererType from '@jbrowse/core/pluggableElementTypes/renderers/FeatureRendererType'

export default class extends FeatureRendererType {}
45 changes: 45 additions & 0 deletions plugins/arc/src/ArcRenderer/ArcRendering.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import SimpleFeature from '@jbrowse/core/util/simpleFeature'
import React from 'react'
import { render } from '@testing-library/react'
import Rendering from './ArcRendering'

test('no features', () => {
const { container } = render(
<Rendering
width={500}
height={500}
regions={[{ refName: 'zonk', start: 0, end: 300 }]}
blockKey={1}
config={{}}
bpPerPx={3}
displayModel={{}}
features={new Map()}
/>,
)

expect(container.firstChild).toMatchSnapshot()
})

test('one feature', () => {
const { container } = render(
<Rendering
width={500}
height={500}
regions={[{ refName: 'zonk', start: 0, end: 1000 }]}
blockKey={1}
features={
new Map([
[
'one',
new SimpleFeature({ uniqueId: 'one', score: 10, start: 1, end: 3 }),
],
])
}
config={{ type: 'DummyRenderer' }}
bpPerPx={3}
displayModel={{}}
/>,
)

expect(container.firstChild).toMatchSnapshot()
})
131 changes: 131 additions & 0 deletions plugins/arc/src/ArcRenderer/ArcRendering.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
import React from 'react'
import { readConfObject } from '@jbrowse/core/configuration'
import { bpSpanPx, measureText } from '@jbrowse/core/util'
import { observer } from 'mobx-react'
import { Tooltip } from 'react-svg-tooltip'

// eslint-disable-next-line @typescript-eslint/no-explicit-any
function ArcRendering(props: any) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const onClick = (event: any, id: any) => {
const { onFeatureClick: handler } = props
if (!handler) {
return undefined
}
return handler(event, id)
}

const {
features,
config,
regions,
blockKey,
bpPerPx,
displayModel: { selectedFeatureId },
} = props
const [region] = regions
const arcsRendered = []

for (const feature of features.values()) {
const [left, right] = bpSpanPx(
feature.get('start'),
feature.get('end'),
region,
bpPerPx,
)

const featureId = feature.id()
const id = blockKey + '-' + featureId
let stroke = readConfObject(config, 'color', { feature })
let textStroke = 'black'
if (
selectedFeatureId &&
String(selectedFeatureId) === String(feature.id())
) {
stroke = textStroke = 'red'
}
const label = readConfObject(config, 'label', { feature })
const caption = readConfObject(config, 'caption', { feature })
const strokeWidth = readConfObject(config, 'thickness', { feature }) || 1
const height = readConfObject(config, 'height', { feature }) || 100
const ref = React.createRef<SVGPathElement>()
const tooltipWidth = 20 + measureText(caption?.toString())

const t = 0.5
// formula: https://en.wikipedia.org/wiki/B%C3%A9zier_curve#Cubic_B%C3%A9zier_curves
const textYCoord =
(1 - t) * (1 - t) * (1 - t) * 0 +
3 * ((1 - t) * (1 - t)) * (t * height) +
3 * (1 - t) * (t * t) * height +
t * t * t * 0

arcsRendered.push(
<g key={id} onClick={e => onClick(e, featureId)}>
<path
id={id}
d={`M ${left} 0 C ${left} ${height}, ${right} ${height}, ${right} 0`}
stroke={stroke}
strokeWidth={strokeWidth}
fill="transparent"
onClick={e => onClick(e, featureId)}
ref={ref}
pointerEvents="stroke"
/>
<Tooltip triggerRef={ref}>
<rect
x={12}
y={0}
width={tooltipWidth}
height={20}
rx={5}
ry={5}
fill="black"
fillOpacity="50%"
/>
<text
x={22}
y={14}
fontSize={10}
fill="white"
textLength={tooltipWidth - 20}
>
{caption}
</text>
</Tooltip>
<text
x={left + (right - left) / 2}
y={textYCoord + 3}
style={{ stroke: 'white', strokeWidth: '0.6em' }}
>
{label}
</text>
<text
x={left + (right - left) / 2}
y={textYCoord + 3}
style={{ stroke: textStroke }}
>
{label}
</text>
</g>,
)
}

const width = (region.end - region.start) / bpPerPx
const height = 500

return (
<svg
className="ArcRendering"
width={width}
height={height}
style={{
outline: 'none',
position: 'relative',
}}
>
{arcsRendered}
</svg>
)
}

export default observer(ArcRendering)
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`no features 1`] = `
<svg
class="ArcRendering"
height="500"
style="outline: none; position: relative;"
width="100"
/>
`;

exports[`one feature 1`] = `
<svg
class="ArcRendering"
height="500"
style="outline: none; position: relative;"
width="333.3333333333333"
>
<g>
<path
d="M 0.3 0 C 0.3 100, 1 100, 1 0"
fill="transparent"
id="1-one"
pointer-events="stroke"
stroke-width="1"
/>
<g />
<text
style="stroke: white; stroke-width: 0.6em;"
x="0.6499999999999999"
y="78"
/>
<text
style="stroke: black;"
x="0.6499999999999999"
y="78"
/>
</g>
</svg>
`;
36 changes: 36 additions & 0 deletions plugins/arc/src/ArcRenderer/configSchema.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { ConfigurationSchema } from '@jbrowse/core/configuration'

export default ConfigurationSchema(
'ArcRenderer',
{
color: {
type: 'color',
description: 'the color of the arcs',
defaultValue: 'darkblue',
},
thickness: {
type: 'number',
description: 'the thickness of the arcs',
defaultValue: `jexl:logThickness(feature,'score')`,
},
label: {
type: 'string',
description: 'the label to appear at the apex of the arcs',
defaultValue: `jexl:get(feature,'score')`,
contextVariable: ['feature'],
},
height: {
type: 'number',
description: 'the height of the arcs',
defaultValue: `jexl:log10(get(feature,'end')-get(feature,'start'))*50`,
},
caption: {
type: 'string',
description:
'the caption to appear when hovering over any point on the arcs',
defaultValue: `jexl:get(feature,'name')`,
contextVariable: ['feature'],
},
},
{ explicitlyTyped: true },
)
3 changes: 3 additions & 0 deletions plugins/arc/src/ArcRenderer/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export { default as ReactComponent } from './ArcRendering'
export { default as configSchema } from './configSchema'
export { default } from './ArcRenderer'
24 changes: 24 additions & 0 deletions plugins/arc/src/LinearArcDisplay/configSchema.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import PluginManager from '@jbrowse/core/PluginManager'
import { types } from 'mobx-state-tree'
import { ConfigurationSchema } from '@jbrowse/core/configuration'

export function configSchemaFactory(pluginManager: PluginManager) {
const LGVPlugin = pluginManager.getPlugin(
'LinearGenomeViewPlugin',
) as import('@jbrowse/plugin-linear-genome-view').default
// @ts-ignore
const { baseLinearDisplayConfigSchema } = LGVPlugin.exports
return ConfigurationSchema(
'LinearArcDisplay',
{
renderer: types.optional(
pluginManager.pluggableConfigSchemaType('renderer'),
{ type: 'ArcRenderer' },
),
},
{
baseConfiguration: baseLinearDisplayConfigSchema,
explicitlyTyped: true,
},
)
}
2 changes: 2 additions & 0 deletions plugins/arc/src/LinearArcDisplay/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export { configSchemaFactory } from './configSchema'
export { stateModelFactory } from './model'
Loading

0 comments on commit 3dabffa

Please sign in to comment.