Skip to content

Commit

Permalink
Add floating labels to SVG features (#2781)
Browse files Browse the repository at this point in the history
* Increase min width from 1 to 2px

* FeatureLabel observable

* Move logic to the feature label itself

* Smooth over block boundaries
  • Loading branch information
cmdcolin committed Apr 7, 2022
1 parent f365543 commit 9d38654
Show file tree
Hide file tree
Showing 13 changed files with 193 additions and 123 deletions.
14 changes: 14 additions & 0 deletions packages/core/util/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1135,3 +1135,17 @@ export function getBpDisplayStr(totalBp: number) {
}
return str
}

export function getViewParams(model: IAnyStateTreeNode, exportSVG?: boolean) {
// @ts-ignore
const { dynamicBlocks, staticBlocks, offsetPx } = getContainingView(model)
const block = dynamicBlocks?.contentBlocks[0] || {}
const staticblock = staticBlocks?.contentBlocks[0] || {}
const staticblock1 = staticBlocks?.contentBlocks[1] || {}
return {
offsetPx: exportSVG ? 0 : offsetPx - staticblock.offsetPx,
offsetPx1: exportSVG ? 0 : offsetPx - staticblock1.offsetPx,
start: block.start,
end: block.end,
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
isAbortException,
getContainingView,
getSession,
getViewParams,
isSelectionContainer,
isSessionModelWithWidgets,
} from '@jbrowse/core/util'
Expand Down Expand Up @@ -623,6 +624,7 @@ export const BaseLinearDisplay = types
return rendererType.renderInClient(rpcManager, {
...renderArgs,
...renderProps,
viewParams: getViewParams(self, true),
exportSVG: opts,
})
}),
Expand Down
Original file line number Diff line number Diff line change
@@ -1,20 +1,21 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import React from 'react'
import { types, getParent, isAlive, cast, Instance } from 'mobx-state-tree'
import { readConfObject } from '@jbrowse/core/configuration'
import { Feature } from '@jbrowse/core/util/simpleFeature'
import {
assembleLocString,
getSession,
getContainingDisplay,
getViewParams,
makeAbortableReaction,
Feature,
} from '@jbrowse/core/util'
import { Region } from '@jbrowse/core/util/types/mst'
import {
AbstractDisplayModel,
isRetryException,
} from '@jbrowse/core/util/types'
import React from 'react'

import {
assembleLocString,
makeAbortableReaction,
getSession,
getContainingDisplay,
} from '@jbrowse/core/util'
import {
getTrackAssemblyNames,
getRpcSessionId,
Expand Down Expand Up @@ -296,6 +297,7 @@ async function renderBlockEffect(
await rendererType.renderInClient(rpcManager, {
...renderArgs,
...renderProps,
viewParams: getViewParams(self),
signal,
})
return {
Expand Down
35 changes: 17 additions & 18 deletions plugins/linear-genome-view/src/LinearBasicDisplay/model.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,17 @@
import { lazy } from 'react'
import { ConfigurationReference, getConf } from '@jbrowse/core/configuration'
import {
getConf,
ConfigurationReference,
AnyConfigurationSchemaType,
} 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'))
Expand All @@ -29,31 +36,23 @@ const stateModelFactory = (configSchema: AnyConfigurationSchemaType) =>
},

get showLabels() {
const showLabels = getConf(self, ['renderer', 'showLabels'])
return self.trackShowLabels !== undefined
? self.trackShowLabels
: showLabels
return self.trackShowLabels ?? getConf(self, ['renderer', 'showLabels'])
},

get showDescriptions() {
const showDescriptions = getConf(self, ['renderer', 'showLabels'])
return self.trackShowDescriptions !== undefined
? self.trackShowDescriptions
: showDescriptions
return (
self.trackShowDescriptions ??
getConf(self, ['renderer', 'showLabels'])
)
},

get maxHeight() {
const maxHeight = getConf(self, ['renderer', 'maxHeight'])
return self.trackMaxHeight !== undefined
? self.trackMaxHeight
: maxHeight
return self.trackMaxHeight ?? getConf(self, ['renderer', 'maxHeight'])
},

get displayMode() {
const displayMode = getConf(self, ['renderer', 'displayMode'])
return self.trackDisplayMode !== undefined
? self.trackDisplayMode
: displayMode
return self.trackDisplayMode ?? displayMode
},
get rendererConfig() {
const configBlob = getConf(self, ['renderer']) || {}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1444,7 +1444,6 @@ exports[`<LinearGenomeView /> renders two tracks, two regions 1`] = `
style="width: 100px;"
>
<svg
class="SvgFeatureRendering"
data-testid="svgfeatures"
height="100"
width="100"
Expand All @@ -1459,7 +1458,6 @@ exports[`<LinearGenomeView /> renders two tracks, two regions 1`] = `
style="width: 1000px;"
>
<svg
class="SvgFeatureRendering"
data-testid="svgfeatures"
height="100"
width="1000"
Expand Down Expand Up @@ -1589,7 +1587,6 @@ exports[`<LinearGenomeView /> renders two tracks, two regions 1`] = `
style="width: 100px;"
>
<svg
class="SvgFeatureRendering"
data-testid="svgfeatures"
height="100"
width="100"
Expand All @@ -1604,7 +1601,6 @@ exports[`<LinearGenomeView /> renders two tracks, two regions 1`] = `
style="width: 1000px;"
>
<svg
class="SvgFeatureRendering"
data-testid="svgfeatures"
height="100"
width="1000"
Expand Down
2 changes: 1 addition & 1 deletion plugins/svg/src/SvgFeatureRenderer/components/Box.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ function Box(props: {
}
const leftWithinBlock = Math.max(left, 0)
const diff = leftWithinBlock - left
const widthWithinBlock = Math.max(1, Math.min(width - diff, screenWidth))
const widthWithinBlock = Math.max(2, Math.min(width - diff, screenWidth))

// if feature has parent and type is intron, then don't render the intron
// subfeature (if it doesn't have a parent, then maybe the introns are
Expand Down
34 changes: 17 additions & 17 deletions plugins/svg/src/SvgFeatureRenderer/components/FeatureGlyph.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,13 @@ import {
AnyConfigurationModel,
readConfObject,
} from '@jbrowse/core/configuration'
import { Feature, Region } from '@jbrowse/core/util'
import { SceneGraph } from '@jbrowse/core/util/layouts'
import { observer } from 'mobx-react'

// locals
import type { DisplayModel } from './util'
import FeatureLabel from './FeatureLabel'
import { Feature } from '@jbrowse/core/util/simpleFeature'
import { SceneGraph } from '@jbrowse/core/util/layouts'

function FeatureGlyph(props: {
feature: Feature
Expand All @@ -18,22 +21,27 @@ function FeatureGlyph(props: {
shouldShowDescription: boolean
fontHeight: number
allowedWidthExpansion: number
displayModel: DisplayModel
selected?: boolean
reversed?: boolean
topLevel?: boolean
region: Region
viewParams: {
end: number
start: number
offsetPx: number
offsetPx1: number
}
bpPerPx: number
}) {
const {
feature,
rootLayout,
selected,
config,
name,
description,
shouldShowName,
shouldShowDescription,
fontHeight,
allowedWidthExpansion,
reversed,
} = props

const featureLayout = rootLayout.getSubRecord(String(feature.id()))
Expand All @@ -44,21 +52,15 @@ function FeatureGlyph(props: {

return (
<g>
<GlyphComponent
featureLayout={featureLayout}
selected={selected}
{...props}
/>
<GlyphComponent featureLayout={featureLayout} {...props} />
{shouldShowName ? (
<FeatureLabel
text={name}
x={rootLayout.getSubRecord('nameLabel')?.absolute.left || 0}
y={rootLayout.getSubRecord('nameLabel')?.absolute.top || 0}
color={readConfObject(config, ['labels', 'nameColor'], { feature })}
fontHeight={fontHeight}
reversed={reversed}
featureWidth={featureLayout.width}
allowedWidthExpansion={allowedWidthExpansion}
{...props}
/>
) : null}
{shouldShowDescription ? (
Expand All @@ -69,10 +71,8 @@ function FeatureGlyph(props: {
color={readConfObject(config, ['labels', 'descriptionColor'], {
feature,
})}
fontHeight={fontHeight}
featureWidth={featureLayout.width}
reversed={reversed}
allowedWidthExpansion={allowedWidthExpansion}
{...props}
/>
) : null}
</g>
Expand Down
105 changes: 70 additions & 35 deletions plugins/svg/src/SvgFeatureRenderer/components/FeatureLabel.tsx
Original file line number Diff line number Diff line change
@@ -1,46 +1,81 @@
import React from 'react'
import { measureText } from '@jbrowse/core/util'
import { observer } from 'mobx-react'
import { isStateTreeNode } from 'mobx-state-tree'
import { measureText, getViewParams, Feature, Region } from '@jbrowse/core/util'
import { DisplayModel } from './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 {
export default observer(
({
text,
x,
y,
region,
reversed,
bpPerPx,
feature,
viewParams,
color = 'black',
fontHeight = 13,
featureWidth = 0,
reversed,
allowedWidthExpansion,
fontWidthScaleFactor = 0.6,
} = props
allowedWidthExpansion = 0,
displayModel = {},
}: {
text: string
x: number
y: number
color?: string
fontHeight?: number
featureWidth?: number
bpPerPx: number
allowedWidthExpansion?: number
feature: Feature
reversed?: boolean
displayModel: DisplayModel
region: Region
viewParams: {
start: number
end: number
offsetPx: number
offsetPx1: number
}
}) => {
const totalWidth = featureWidth + allowedWidthExpansion
const measuredTextWidth = measureText(text, fontHeight)
const params = isStateTreeNode(displayModel)
? getViewParams(displayModel)
: viewParams

const viewLeft = reversed ? params.end : params.start

const rstart = region.start
const rend = region.end
const fstart = feature.get('start')
const fend = feature.get('end')

const fontWidth = fontHeight * fontWidthScaleFactor
const totalWidth =
featureWidth && allowedWidthExpansion
? featureWidth + allowedWidthExpansion
: Infinity
const featureWidthBp = measuredTextWidth * bpPerPx

const measuredTextWidth = measureText(text, fontHeight)
// this tricky bit of code helps smooth over block boundaries
// not supported for reverse mode currently
// reason: reverse mode allocates space for the label in the "normal
// forward orientation" making it hard to slide. The reverse mode should
// allocate the label space in the reverse orientation to slide it
if (
viewLeft < rend &&
viewLeft > rstart &&
fstart < viewLeft &&
viewLeft + featureWidthBp < fend
) {
x = params.offsetPx
} else if (fstart < viewLeft && viewLeft + featureWidthBp < fend) {
x = params.offsetPx1
}

return (
<text
x={reversed ? x + featureWidth - measuredTextWidth : x}
y={y + fontHeight}
style={{ fontSize: fontHeight, fill: color, cursor: 'default' }}
>
{measuredTextWidth > totalWidth
? `${text.slice(0, totalWidth / fontWidth)}...`
: text}
</text>
)
}
return (
<text x={x} y={y + fontHeight} fill={color} fontSize={fontHeight}>
{measuredTextWidth > totalWidth
? `${text.slice(0, totalWidth / (fontHeight * 0.6))}...`
: text}
</text>
)
},
)
4 changes: 2 additions & 2 deletions plugins/svg/src/SvgFeatureRenderer/components/Subfeatures.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,14 @@ import {
readConfObject,
} from '@jbrowse/core/configuration'
import { observer } from 'mobx-react'
import { Feature } from '@jbrowse/core/util/simpleFeature'
import { SceneGraph } from '@jbrowse/core/util/layouts'
import {
chooseGlyphComponent,
ExtraGlyphValidator,
layOut,
layOutFeature,
} from './util'
import { Feature } from '@jbrowse/core/util/simpleFeature'
import { SceneGraph } from '@jbrowse/core/util/layouts'

function Subfeatures(props: {
feature: Feature
Expand Down
Loading

0 comments on commit 9d38654

Please sign in to comment.