@jbrowse/plugin-lollipop 2.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +201 -0
- package/dist/LinearLollipopDisplay/configSchema.d.ts +26 -0
- package/dist/LinearLollipopDisplay/configSchema.js +13 -0
- package/dist/LinearLollipopDisplay/configSchema.js.map +1 -0
- package/dist/LinearLollipopDisplay/index.d.ts +2 -0
- package/dist/LinearLollipopDisplay/index.js +8 -0
- package/dist/LinearLollipopDisplay/index.js.map +1 -0
- package/dist/LinearLollipopDisplay/model.d.ts +303 -0
- package/dist/LinearLollipopDisplay/model.js +36 -0
- package/dist/LinearLollipopDisplay/model.js.map +1 -0
- package/dist/LollipopRenderer/Layout.d.ts +55 -0
- package/dist/LollipopRenderer/Layout.js +116 -0
- package/dist/LollipopRenderer/Layout.js.map +1 -0
- package/dist/LollipopRenderer/LollipopRenderer.d.ts +13 -0
- package/dist/LollipopRenderer/LollipopRenderer.js +52 -0
- package/dist/LollipopRenderer/LollipopRenderer.js.map +1 -0
- package/dist/LollipopRenderer/components/Lollipop.d.ts +3 -0
- package/dist/LollipopRenderer/components/Lollipop.js +63 -0
- package/dist/LollipopRenderer/components/Lollipop.js.map +1 -0
- package/dist/LollipopRenderer/components/LollipopRendering.d.ts +3 -0
- package/dist/LollipopRenderer/components/LollipopRendering.js +80 -0
- package/dist/LollipopRenderer/components/LollipopRendering.js.map +1 -0
- package/dist/LollipopRenderer/components/ScoreText.d.ts +15 -0
- package/dist/LollipopRenderer/components/ScoreText.js +20 -0
- package/dist/LollipopRenderer/components/ScoreText.js.map +1 -0
- package/dist/LollipopRenderer/components/Stick.d.ts +15 -0
- package/dist/LollipopRenderer/components/Stick.js +12 -0
- package/dist/LollipopRenderer/components/Stick.js.map +1 -0
- package/dist/LollipopRenderer/configSchema.d.ts +56 -0
- package/dist/LollipopRenderer/configSchema.js +59 -0
- package/dist/LollipopRenderer/configSchema.js.map +1 -0
- package/dist/LollipopRenderer/index.d.ts +3 -0
- package/dist/LollipopRenderer/index.js +13 -0
- package/dist/LollipopRenderer/index.js.map +1 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +60 -0
- package/dist/index.js.map +1 -0
- package/esm/LinearLollipopDisplay/configSchema.d.ts +26 -0
- package/esm/LinearLollipopDisplay/configSchema.js +9 -0
- package/esm/LinearLollipopDisplay/configSchema.js.map +1 -0
- package/esm/LinearLollipopDisplay/index.d.ts +2 -0
- package/esm/LinearLollipopDisplay/index.js +3 -0
- package/esm/LinearLollipopDisplay/index.js.map +1 -0
- package/esm/LinearLollipopDisplay/model.d.ts +303 -0
- package/esm/LinearLollipopDisplay/model.js +32 -0
- package/esm/LinearLollipopDisplay/model.js.map +1 -0
- package/esm/LollipopRenderer/Layout.d.ts +55 -0
- package/esm/LollipopRenderer/Layout.js +111 -0
- package/esm/LollipopRenderer/Layout.js.map +1 -0
- package/esm/LollipopRenderer/LollipopRenderer.d.ts +13 -0
- package/esm/LollipopRenderer/LollipopRenderer.js +23 -0
- package/esm/LollipopRenderer/LollipopRenderer.js.map +1 -0
- package/esm/LollipopRenderer/components/Lollipop.d.ts +3 -0
- package/esm/LollipopRenderer/components/Lollipop.js +58 -0
- package/esm/LollipopRenderer/components/Lollipop.js.map +1 -0
- package/esm/LollipopRenderer/components/LollipopRendering.d.ts +3 -0
- package/esm/LollipopRenderer/components/LollipopRendering.js +75 -0
- package/esm/LollipopRenderer/components/LollipopRendering.js.map +1 -0
- package/esm/LollipopRenderer/components/ScoreText.d.ts +15 -0
- package/esm/LollipopRenderer/components/ScoreText.js +14 -0
- package/esm/LollipopRenderer/components/ScoreText.js.map +1 -0
- package/esm/LollipopRenderer/components/Stick.d.ts +15 -0
- package/esm/LollipopRenderer/components/Stick.js +7 -0
- package/esm/LollipopRenderer/components/Stick.js.map +1 -0
- package/esm/LollipopRenderer/configSchema.d.ts +56 -0
- package/esm/LollipopRenderer/configSchema.js +57 -0
- package/esm/LollipopRenderer/configSchema.js.map +1 -0
- package/esm/LollipopRenderer/index.d.ts +3 -0
- package/esm/LollipopRenderer/index.js +4 -0
- package/esm/LollipopRenderer/index.js.map +1 -0
- package/esm/index.d.ts +6 -0
- package/esm/index.js +31 -0
- package/esm/index.js.map +1 -0
- package/package.json +56 -0
- package/src/LinearLollipopDisplay/configSchema.ts +14 -0
- package/src/LinearLollipopDisplay/index.ts +2 -0
- package/src/LinearLollipopDisplay/model.ts +40 -0
- package/src/LollipopRenderer/Layout.ts +172 -0
- package/src/LollipopRenderer/LollipopRenderer.js +29 -0
- package/src/LollipopRenderer/components/Lollipop.tsx +113 -0
- package/src/LollipopRenderer/components/LollipopRendering.test.js +45 -0
- package/src/LollipopRenderer/components/LollipopRendering.tsx +140 -0
- package/src/LollipopRenderer/components/ScoreText.tsx +43 -0
- package/src/LollipopRenderer/components/Stick.tsx +36 -0
- package/src/LollipopRenderer/components/__snapshots__/LollipopRendering.test.js.snap +37 -0
- package/src/LollipopRenderer/configSchema.ts +63 -0
- package/src/LollipopRenderer/index.ts +3 -0
- package/src/index.ts +41 -0
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import { readConfObject } from '@jbrowse/core/configuration'
|
|
2
|
+
import { doesIntersect2 } from '@jbrowse/core/util/range'
|
|
3
|
+
import { AnyConfigurationModel } from '@jbrowse/core/configuration'
|
|
4
|
+
|
|
5
|
+
interface LayoutItem {
|
|
6
|
+
uniqueId: string
|
|
7
|
+
anchorLocation: number
|
|
8
|
+
width: number
|
|
9
|
+
height: number
|
|
10
|
+
data: { score: number }
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
type LayoutEntry = LayoutItem & { x: number; y: number }
|
|
14
|
+
|
|
15
|
+
type LayoutMap = Map<string, LayoutEntry>
|
|
16
|
+
|
|
17
|
+
export class FloatingLayout {
|
|
18
|
+
width: number
|
|
19
|
+
|
|
20
|
+
totalHeight = 0
|
|
21
|
+
|
|
22
|
+
constructor({ width }: { width: number }) {
|
|
23
|
+
if (!width) {
|
|
24
|
+
throw new Error('width required to make a new FloatingLayout')
|
|
25
|
+
}
|
|
26
|
+
this.width = width
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
items: LayoutItem[] = []
|
|
30
|
+
|
|
31
|
+
layout: LayoutMap = new Map()
|
|
32
|
+
|
|
33
|
+
layoutDirty = false
|
|
34
|
+
|
|
35
|
+
add(
|
|
36
|
+
uniqueId: string,
|
|
37
|
+
anchorLocation: number,
|
|
38
|
+
width: number,
|
|
39
|
+
height: number,
|
|
40
|
+
data: { score: number },
|
|
41
|
+
) {
|
|
42
|
+
this.items.push({ uniqueId, anchorLocation, width, height, data })
|
|
43
|
+
this.layoutDirty = true
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* @returns Map of `uniqueId => {x,y,anchorLocation,width,height,data}`
|
|
48
|
+
*/
|
|
49
|
+
getLayout(configuration?: AnyConfigurationModel) {
|
|
50
|
+
if (!this.layoutDirty) {
|
|
51
|
+
return this.layout
|
|
52
|
+
}
|
|
53
|
+
if (!configuration) {
|
|
54
|
+
throw new Error('configuration object required')
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const minY = readConfObject(configuration, 'minStickLength')
|
|
58
|
+
|
|
59
|
+
// sort them by score ascending, so higher scores will always end up
|
|
60
|
+
// stacked last (toward the bottom)
|
|
61
|
+
const sorted = this.items.sort((a, b) => a.data.score - b.data.score)
|
|
62
|
+
|
|
63
|
+
// bump them
|
|
64
|
+
let maxBottom = 0
|
|
65
|
+
const layoutEntries: [string, LayoutEntry][] = new Array(sorted.length)
|
|
66
|
+
for (let i = 0; i < sorted.length; i += 1) {
|
|
67
|
+
const currentItem = sorted[i]
|
|
68
|
+
const { anchorLocation, width, height } = currentItem
|
|
69
|
+
const start = anchorLocation - width / 2
|
|
70
|
+
const end = start + width
|
|
71
|
+
let top = minY
|
|
72
|
+
let bottom = top + height
|
|
73
|
+
|
|
74
|
+
// figure out how far down to put it
|
|
75
|
+
for (let j = 0; j < i; j += 1) {
|
|
76
|
+
const [, previouslyLaidOutItem] = layoutEntries[j]
|
|
77
|
+
const {
|
|
78
|
+
x: prevStart,
|
|
79
|
+
y: prevTop,
|
|
80
|
+
width: prevWidth,
|
|
81
|
+
height: prevHeight,
|
|
82
|
+
} = previouslyLaidOutItem
|
|
83
|
+
const prevEnd = prevStart + prevWidth
|
|
84
|
+
const prevBottom = prevTop + prevHeight
|
|
85
|
+
if (
|
|
86
|
+
doesIntersect2(prevStart, prevEnd, start, end) &&
|
|
87
|
+
doesIntersect2(prevTop, prevBottom, top, bottom)
|
|
88
|
+
) {
|
|
89
|
+
// bump this one to the bottom of the previous one
|
|
90
|
+
top = prevBottom
|
|
91
|
+
bottom = top + height
|
|
92
|
+
j = -1 // we need to check all of them again after bumping
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// record the entry and update the maxBottom
|
|
97
|
+
layoutEntries[i] = [
|
|
98
|
+
currentItem.uniqueId,
|
|
99
|
+
{ ...currentItem, x: start, y: top },
|
|
100
|
+
]
|
|
101
|
+
if (bottom > maxBottom) {
|
|
102
|
+
maxBottom = bottom
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// try to tile them left to right all at the same level
|
|
107
|
+
// if they don't fit, try to alternate them on 2 levels, then 3
|
|
108
|
+
this.totalHeight = maxBottom
|
|
109
|
+
this.layout = new Map(layoutEntries)
|
|
110
|
+
this.layoutDirty = false
|
|
111
|
+
return this.layout
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
getTotalHeight() {
|
|
115
|
+
if (this.layoutDirty) {
|
|
116
|
+
throw new Error('getTotalHeight does not work when the layout is dirty.')
|
|
117
|
+
}
|
|
118
|
+
return this.totalHeight
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
serializeRegion() {
|
|
122
|
+
return this.toJSON()
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
toJSON() {
|
|
126
|
+
if (this.layoutDirty) {
|
|
127
|
+
throw new Error('toJSON does not work when the layout is dirty.')
|
|
128
|
+
}
|
|
129
|
+
return { pairs: [...this.getLayout()], totalHeight: this.getTotalHeight() }
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
static fromJSON() {
|
|
133
|
+
throw new Error('not supported')
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export class PrecomputedFloatingLayout {
|
|
138
|
+
layout: LayoutMap
|
|
139
|
+
|
|
140
|
+
totalHeight: number
|
|
141
|
+
|
|
142
|
+
constructor({
|
|
143
|
+
pairs,
|
|
144
|
+
totalHeight,
|
|
145
|
+
}: {
|
|
146
|
+
pairs: [string, LayoutEntry][]
|
|
147
|
+
totalHeight: number
|
|
148
|
+
}) {
|
|
149
|
+
this.layout = new Map(pairs)
|
|
150
|
+
this.totalHeight = totalHeight
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
add(uniqueId: string) {
|
|
154
|
+
if (!this.layout.has(uniqueId)) {
|
|
155
|
+
throw new Error(`layout error, precomputed layout is missing ${uniqueId}`)
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
getLayout() {
|
|
160
|
+
return this.layout
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
getTotalHeight() {
|
|
164
|
+
return this.totalHeight
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
static fromJSON(
|
|
168
|
+
json: ConstructorParameters<typeof PrecomputedFloatingLayout>[0],
|
|
169
|
+
) {
|
|
170
|
+
return new PrecomputedFloatingLayout(json)
|
|
171
|
+
}
|
|
172
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import BoxRendererType, {
|
|
2
|
+
LayoutSession,
|
|
3
|
+
} from '@jbrowse/core/pluggableElementTypes/renderers/BoxRendererType'
|
|
4
|
+
import MultiLayout from '@jbrowse/core/util/layouts/MultiLayout'
|
|
5
|
+
import { FloatingLayout, PrecomputedFloatingLayout } from './Layout'
|
|
6
|
+
|
|
7
|
+
class FloatingLayoutSession extends LayoutSession {
|
|
8
|
+
makeLayout() {
|
|
9
|
+
'sequenceAdapter'
|
|
10
|
+
|
|
11
|
+
const { end, start } = this.regions[0]
|
|
12
|
+
const widthPx = (end - start) / this.bpPerPx
|
|
13
|
+
return new MultiLayout(FloatingLayout, { width: widthPx })
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
layoutIsValid(/* layout */) {
|
|
17
|
+
return false // layout.left layout.width === this.width
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export default class extends BoxRendererType {
|
|
22
|
+
createSession(args) {
|
|
23
|
+
return new FloatingLayoutSession(args)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
deserializeLayoutInClient(json) {
|
|
27
|
+
return new PrecomputedFloatingLayout(json)
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import { readConfObject } from '@jbrowse/core/configuration'
|
|
3
|
+
import { observer } from 'mobx-react'
|
|
4
|
+
import ScoreText from './ScoreText'
|
|
5
|
+
|
|
6
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
7
|
+
export default observer(function Lollipop(props: Record<string, any>) {
|
|
8
|
+
const { feature, config, layoutRecord, selectedFeatureId } = props
|
|
9
|
+
const {
|
|
10
|
+
anchorLocation,
|
|
11
|
+
y,
|
|
12
|
+
data: { radiusPx },
|
|
13
|
+
} = layoutRecord
|
|
14
|
+
|
|
15
|
+
const onFeatureMouseDown = (event: React.MouseEvent) => {
|
|
16
|
+
const { onFeatureMouseDown: handler, feature } = props
|
|
17
|
+
return handler?.(event, feature.id())
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const onFeatureMouseEnter = (event: React.MouseEvent) => {
|
|
21
|
+
const { onFeatureMouseEnter: handler, feature } = props
|
|
22
|
+
return handler?.(event, feature.id())
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const onFeatureMouseOut = (event: React.MouseEvent | React.FocusEvent) => {
|
|
26
|
+
const { onFeatureMouseOut: handler, feature } = props
|
|
27
|
+
return handler?.(event, feature.id())
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const onFeatureMouseOver = (event: React.MouseEvent | React.FocusEvent) => {
|
|
31
|
+
const { onFeatureMouseOver: handler, feature } = props
|
|
32
|
+
return handler?.(event, feature.id())
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const onFeatureMouseUp = (event: React.MouseEvent) => {
|
|
36
|
+
const { onFeatureMouseUp: handler, feature } = props
|
|
37
|
+
return handler?.(event, feature.id())
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const onFeatureMouseLeave = (event: React.MouseEvent) => {
|
|
41
|
+
const { onFeatureMouseLeave: handler, feature } = props
|
|
42
|
+
return handler?.(event, feature.id())
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const onFeatureMouseMove = (event: React.MouseEvent) => {
|
|
46
|
+
const { onFeatureMouseMove: handler, feature } = props
|
|
47
|
+
return handler?.(event, feature.id())
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const onFeatureClick = (event: React.MouseEvent) => {
|
|
51
|
+
const { onFeatureClick: handler, feature } = props
|
|
52
|
+
event.stopPropagation()
|
|
53
|
+
return handler?.(event, feature.id())
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const styleOuter = {
|
|
57
|
+
fill: readConfObject(config, 'strokeColor', { feature }),
|
|
58
|
+
}
|
|
59
|
+
if (String(selectedFeatureId) === String(feature.id())) {
|
|
60
|
+
styleOuter.fill = 'red'
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const styleInner = {
|
|
64
|
+
fill: readConfObject(config, 'innerColor', { feature }),
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const strokeWidth = readConfObject(config, 'strokeWidth', { feature })
|
|
68
|
+
|
|
69
|
+
return (
|
|
70
|
+
<g data-testid={feature.id()}>
|
|
71
|
+
<title>{readConfObject(config, 'caption', { feature })}</title>
|
|
72
|
+
<circle
|
|
73
|
+
cx={anchorLocation}
|
|
74
|
+
cy={y + radiusPx}
|
|
75
|
+
r={radiusPx}
|
|
76
|
+
style={styleOuter}
|
|
77
|
+
onMouseDown={onFeatureMouseDown}
|
|
78
|
+
onMouseEnter={onFeatureMouseEnter}
|
|
79
|
+
onMouseOut={onFeatureMouseOut}
|
|
80
|
+
onMouseOver={onFeatureMouseOver}
|
|
81
|
+
onMouseUp={onFeatureMouseUp}
|
|
82
|
+
onMouseLeave={onFeatureMouseLeave}
|
|
83
|
+
onMouseMove={onFeatureMouseMove}
|
|
84
|
+
onClick={onFeatureClick}
|
|
85
|
+
onFocus={onFeatureMouseOver}
|
|
86
|
+
onBlur={onFeatureMouseOut}
|
|
87
|
+
/>
|
|
88
|
+
{radiusPx - strokeWidth <= 2 ? null : (
|
|
89
|
+
<circle
|
|
90
|
+
cx={anchorLocation}
|
|
91
|
+
cy={y + radiusPx}
|
|
92
|
+
r={radiusPx - strokeWidth}
|
|
93
|
+
style={styleInner}
|
|
94
|
+
onMouseDown={onFeatureMouseDown}
|
|
95
|
+
onMouseEnter={onFeatureMouseEnter}
|
|
96
|
+
onMouseOut={onFeatureMouseOut}
|
|
97
|
+
onMouseOver={onFeatureMouseOver}
|
|
98
|
+
onMouseUp={onFeatureMouseUp}
|
|
99
|
+
onMouseLeave={onFeatureMouseLeave}
|
|
100
|
+
onMouseMove={onFeatureMouseMove}
|
|
101
|
+
onClick={onFeatureClick}
|
|
102
|
+
onFocus={onFeatureMouseOver}
|
|
103
|
+
onBlur={onFeatureMouseOut}
|
|
104
|
+
/>
|
|
105
|
+
)}
|
|
106
|
+
<ScoreText
|
|
107
|
+
feature={feature}
|
|
108
|
+
config={config}
|
|
109
|
+
layoutRecord={layoutRecord}
|
|
110
|
+
/>
|
|
111
|
+
</g>
|
|
112
|
+
)
|
|
113
|
+
})
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import SimpleFeature from '@jbrowse/core/util/simpleFeature'
|
|
2
|
+
import React from 'react'
|
|
3
|
+
import { render } from '@testing-library/react'
|
|
4
|
+
import ConfigSchema from '../configSchema'
|
|
5
|
+
import { FloatingLayout, PrecomputedFloatingLayout } from '../Layout'
|
|
6
|
+
import Rendering from './LollipopRendering'
|
|
7
|
+
|
|
8
|
+
// these tests do very little, let's try to expand them at some point
|
|
9
|
+
test('no features', () => {
|
|
10
|
+
const { container } = render(
|
|
11
|
+
<Rendering
|
|
12
|
+
width={500}
|
|
13
|
+
height={500}
|
|
14
|
+
regions={[{ refName: 'zonk', start: 0, end: 300 }]}
|
|
15
|
+
layout={new PrecomputedFloatingLayout({ pairs: [], totalHeight: 20 })}
|
|
16
|
+
config={{}}
|
|
17
|
+
bpPerPx={3}
|
|
18
|
+
/>,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
expect(container.firstChild).toMatchSnapshot()
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
test('one feature', () => {
|
|
25
|
+
const { container } = render(
|
|
26
|
+
<Rendering
|
|
27
|
+
width={500}
|
|
28
|
+
height={500}
|
|
29
|
+
regions={[{ refName: 'zonk', start: 0, end: 1000 }]}
|
|
30
|
+
layout={new FloatingLayout({ width: 100 })}
|
|
31
|
+
features={
|
|
32
|
+
new Map([
|
|
33
|
+
[
|
|
34
|
+
'one',
|
|
35
|
+
new SimpleFeature({ uniqueId: 'one', score: 10, start: 1, end: 3 }),
|
|
36
|
+
],
|
|
37
|
+
])
|
|
38
|
+
}
|
|
39
|
+
config={ConfigSchema.create({})}
|
|
40
|
+
bpPerPx={3}
|
|
41
|
+
/>,
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
expect(container.firstChild).toMatchSnapshot()
|
|
45
|
+
})
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import {
|
|
3
|
+
AnyConfigurationModel,
|
|
4
|
+
readConfObject,
|
|
5
|
+
} from '@jbrowse/core/configuration'
|
|
6
|
+
import { Feature, Region, bpToPx } from '@jbrowse/core/util'
|
|
7
|
+
import { observer } from 'mobx-react'
|
|
8
|
+
|
|
9
|
+
// locals
|
|
10
|
+
import Lollipop from './Lollipop'
|
|
11
|
+
import Stick from './Stick'
|
|
12
|
+
|
|
13
|
+
function layoutFeat(args: {
|
|
14
|
+
feature: Feature
|
|
15
|
+
bpPerPx: number
|
|
16
|
+
region: Region
|
|
17
|
+
layout: { add: (...args: unknown[]) => void }
|
|
18
|
+
config: AnyConfigurationModel
|
|
19
|
+
}) {
|
|
20
|
+
const { feature, bpPerPx, config, region, layout } = args
|
|
21
|
+
|
|
22
|
+
const centerBp = Math.abs(feature.get('end') + feature.get('start')) / 2
|
|
23
|
+
const centerPx = bpToPx(centerBp, region, bpPerPx)
|
|
24
|
+
const radiusPx = readConfObject(config, 'radius', { feature })
|
|
25
|
+
|
|
26
|
+
if (!radiusPx) {
|
|
27
|
+
console.error(
|
|
28
|
+
new Error(
|
|
29
|
+
`lollipop radius ${radiusPx} configured for feature ${feature.id()}`,
|
|
30
|
+
),
|
|
31
|
+
)
|
|
32
|
+
}
|
|
33
|
+
layout.add(feature.id(), centerPx, radiusPx * 2, radiusPx * 2, {
|
|
34
|
+
featureId: feature.id(),
|
|
35
|
+
anchorX: centerPx,
|
|
36
|
+
radiusPx,
|
|
37
|
+
score: readConfObject(args.config, 'score', { feature }),
|
|
38
|
+
})
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
42
|
+
export default observer(function LollipopRendering(props: Record<string, any>) {
|
|
43
|
+
const onMouseDown = (event: React.MouseEvent) => {
|
|
44
|
+
const { onMouseDown: handler } = props
|
|
45
|
+
return handler?.(event)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const onMouseUp = (event: React.MouseEvent) => {
|
|
49
|
+
const { onMouseUp: handler } = props
|
|
50
|
+
return handler?.(event)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const onMouseEnter = (event: React.MouseEvent | React.FocusEvent) => {
|
|
54
|
+
const { onMouseEnter: handler } = props
|
|
55
|
+
return handler?.(event)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const onMouseLeave = (event: React.MouseEvent | React.FocusEvent) => {
|
|
59
|
+
const { onMouseLeave: handler } = props
|
|
60
|
+
return handler?.(event)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const onMouseOver = (event: React.MouseEvent) => {
|
|
64
|
+
const { onMouseOver: handler } = props
|
|
65
|
+
return handler?.(event)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const onMouseOut = (event: React.MouseEvent) => {
|
|
69
|
+
const { onMouseOut: handler } = props
|
|
70
|
+
return handler?.(event)
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const onClick = (event: React.MouseEvent) => {
|
|
74
|
+
const { onClick: handler } = props
|
|
75
|
+
return handler?.(event)
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const {
|
|
79
|
+
regions,
|
|
80
|
+
bpPerPx,
|
|
81
|
+
layout,
|
|
82
|
+
config,
|
|
83
|
+
features = new Map(),
|
|
84
|
+
displayModel = {},
|
|
85
|
+
} = props
|
|
86
|
+
const { selectedFeatureId } = displayModel
|
|
87
|
+
const [region] = regions
|
|
88
|
+
for (const feature of features.values()) {
|
|
89
|
+
layoutFeat({
|
|
90
|
+
feature,
|
|
91
|
+
bpPerPx,
|
|
92
|
+
region,
|
|
93
|
+
config,
|
|
94
|
+
layout,
|
|
95
|
+
})
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const width = (region.end - region.start) / bpPerPx
|
|
99
|
+
const records = [...layout.getLayout(config).values()]
|
|
100
|
+
const height = layout.getTotalHeight()
|
|
101
|
+
|
|
102
|
+
return (
|
|
103
|
+
<svg
|
|
104
|
+
width={width}
|
|
105
|
+
height={height}
|
|
106
|
+
style={{ position: 'relative' }}
|
|
107
|
+
onMouseDown={onMouseDown}
|
|
108
|
+
onMouseUp={onMouseUp}
|
|
109
|
+
onMouseEnter={onMouseEnter}
|
|
110
|
+
onMouseLeave={onMouseLeave}
|
|
111
|
+
onMouseOver={onMouseOver}
|
|
112
|
+
onMouseOut={onMouseOut}
|
|
113
|
+
onFocus={onMouseEnter}
|
|
114
|
+
onBlur={onMouseLeave}
|
|
115
|
+
onClick={onClick}
|
|
116
|
+
>
|
|
117
|
+
{records.map(layoutRecord => {
|
|
118
|
+
const feature = features.get(layoutRecord.data.featureId)
|
|
119
|
+
return (
|
|
120
|
+
<React.Fragment key={feature.id()}>
|
|
121
|
+
<Stick
|
|
122
|
+
{...props}
|
|
123
|
+
config={config}
|
|
124
|
+
layoutRecord={layoutRecord}
|
|
125
|
+
feature={feature}
|
|
126
|
+
key={`stick-${feature.id()}`}
|
|
127
|
+
/>
|
|
128
|
+
<Lollipop
|
|
129
|
+
{...props}
|
|
130
|
+
layoutRecord={layoutRecord}
|
|
131
|
+
feature={feature}
|
|
132
|
+
key={`body-${feature.id()}`}
|
|
133
|
+
selectedFeatureId={selectedFeatureId}
|
|
134
|
+
/>
|
|
135
|
+
</React.Fragment>
|
|
136
|
+
)
|
|
137
|
+
})}
|
|
138
|
+
</svg>
|
|
139
|
+
)
|
|
140
|
+
})
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import {
|
|
3
|
+
AnyConfigurationModel,
|
|
4
|
+
readConfObject,
|
|
5
|
+
} from '@jbrowse/core/configuration'
|
|
6
|
+
import { contrastingTextColor } from '@jbrowse/core/util/color'
|
|
7
|
+
import { Feature } from '@jbrowse/core/util'
|
|
8
|
+
|
|
9
|
+
export default function ScoreText({
|
|
10
|
+
feature,
|
|
11
|
+
config,
|
|
12
|
+
layoutRecord: {
|
|
13
|
+
y,
|
|
14
|
+
data: { anchorX, radiusPx, score },
|
|
15
|
+
},
|
|
16
|
+
}: {
|
|
17
|
+
feature: Feature
|
|
18
|
+
config: AnyConfigurationModel
|
|
19
|
+
layoutRecord: {
|
|
20
|
+
y: number
|
|
21
|
+
data: { anchorX: number; radiusPx: number; score: number }
|
|
22
|
+
}
|
|
23
|
+
}) {
|
|
24
|
+
const innerColor = readConfObject(config, 'innerColor', { feature })
|
|
25
|
+
|
|
26
|
+
const scoreString = String(score)
|
|
27
|
+
const fontWidth = (radiusPx * 2) / scoreString.length
|
|
28
|
+
const fontHeight = fontWidth * 1.1
|
|
29
|
+
if (fontHeight < 12) {
|
|
30
|
+
return null
|
|
31
|
+
}
|
|
32
|
+
return (
|
|
33
|
+
<text
|
|
34
|
+
style={{ fontSize: fontHeight, fill: contrastingTextColor(innerColor) }}
|
|
35
|
+
x={anchorX}
|
|
36
|
+
y={y + radiusPx - fontHeight / 2.4}
|
|
37
|
+
textAnchor="middle"
|
|
38
|
+
dominantBaseline="hanging"
|
|
39
|
+
>
|
|
40
|
+
{scoreString}
|
|
41
|
+
</text>
|
|
42
|
+
)
|
|
43
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import {
|
|
3
|
+
AnyConfigurationModel,
|
|
4
|
+
readConfObject,
|
|
5
|
+
} from '@jbrowse/core/configuration'
|
|
6
|
+
import { observer } from 'mobx-react'
|
|
7
|
+
import { Feature } from '@jbrowse/core/util'
|
|
8
|
+
|
|
9
|
+
export default observer(function Stick({
|
|
10
|
+
feature,
|
|
11
|
+
config,
|
|
12
|
+
layoutRecord: {
|
|
13
|
+
anchorLocation,
|
|
14
|
+
y,
|
|
15
|
+
data: { radiusPx },
|
|
16
|
+
},
|
|
17
|
+
}: {
|
|
18
|
+
feature: Feature
|
|
19
|
+
config: AnyConfigurationModel
|
|
20
|
+
layoutRecord: {
|
|
21
|
+
anchorLocation: number
|
|
22
|
+
y: number
|
|
23
|
+
data: { radiusPx: number }
|
|
24
|
+
}
|
|
25
|
+
}) {
|
|
26
|
+
return (
|
|
27
|
+
<line
|
|
28
|
+
x1={anchorLocation}
|
|
29
|
+
y1={0}
|
|
30
|
+
x2={anchorLocation}
|
|
31
|
+
y2={y + 2 * radiusPx}
|
|
32
|
+
stroke={readConfObject(config, 'stickColor', { feature })}
|
|
33
|
+
strokeWidth={readConfObject(config, 'stickWidth', { feature })}
|
|
34
|
+
/>
|
|
35
|
+
)
|
|
36
|
+
})
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
|
2
|
+
|
|
3
|
+
exports[`no features 1`] = `
|
|
4
|
+
<svg
|
|
5
|
+
height="20"
|
|
6
|
+
style="position: relative;"
|
|
7
|
+
width="100"
|
|
8
|
+
/>
|
|
9
|
+
`;
|
|
10
|
+
|
|
11
|
+
exports[`one feature 1`] = `
|
|
12
|
+
<svg
|
|
13
|
+
height="16.286652959662007"
|
|
14
|
+
style="position: relative;"
|
|
15
|
+
width="333.3333333333333"
|
|
16
|
+
>
|
|
17
|
+
<line
|
|
18
|
+
stroke="black"
|
|
19
|
+
stroke-width="2"
|
|
20
|
+
x1="0.7"
|
|
21
|
+
x2="0.7"
|
|
22
|
+
y1="0"
|
|
23
|
+
y2="16.286652959662007"
|
|
24
|
+
/>
|
|
25
|
+
<g
|
|
26
|
+
data-testid="one"
|
|
27
|
+
>
|
|
28
|
+
<title />
|
|
29
|
+
<circle
|
|
30
|
+
cx="0.7"
|
|
31
|
+
cy="10.643326479831003"
|
|
32
|
+
r="5.643326479831003"
|
|
33
|
+
style="fill: green;"
|
|
34
|
+
/>
|
|
35
|
+
</g>
|
|
36
|
+
</svg>
|
|
37
|
+
`;
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { ConfigurationSchema } from '@jbrowse/core/configuration'
|
|
2
|
+
|
|
3
|
+
export default ConfigurationSchema(
|
|
4
|
+
'LollipopRenderer',
|
|
5
|
+
{
|
|
6
|
+
strokeColor: {
|
|
7
|
+
type: 'color',
|
|
8
|
+
description: 'the outer color of each lollipop',
|
|
9
|
+
defaultValue: 'green',
|
|
10
|
+
contextVariable: ['feature'],
|
|
11
|
+
},
|
|
12
|
+
innerColor: {
|
|
13
|
+
type: 'color',
|
|
14
|
+
description: 'the inner color of each lollipop',
|
|
15
|
+
defaultValue: '#7fc75f',
|
|
16
|
+
contextVariable: ['feature'],
|
|
17
|
+
},
|
|
18
|
+
strokeWidth: {
|
|
19
|
+
type: 'number',
|
|
20
|
+
description: 'width of the stroked border',
|
|
21
|
+
defaultValue: 4,
|
|
22
|
+
contextVariable: ['feature'],
|
|
23
|
+
},
|
|
24
|
+
radius: {
|
|
25
|
+
type: 'number',
|
|
26
|
+
description: 'radius in pixels of each lollipop body',
|
|
27
|
+
defaultValue: `jexl:sqrt(max(3, (get(feature,'score')*10)/3.14))`,
|
|
28
|
+
contextVariable: ['feature'],
|
|
29
|
+
},
|
|
30
|
+
caption: {
|
|
31
|
+
type: 'string',
|
|
32
|
+
description:
|
|
33
|
+
'the tooltip caption displayed when the mouse hovers over a lollipop',
|
|
34
|
+
defaultValue: `jexl:get(feature,'name')`,
|
|
35
|
+
contextVariable: ['feature'],
|
|
36
|
+
},
|
|
37
|
+
minStickLength: {
|
|
38
|
+
type: 'number',
|
|
39
|
+
description: 'minimum lollipop "stick" length, in pixels',
|
|
40
|
+
defaultValue: 5,
|
|
41
|
+
},
|
|
42
|
+
stickColor: {
|
|
43
|
+
type: 'color',
|
|
44
|
+
description: 'color of the lollipop stick',
|
|
45
|
+
defaultValue: 'black',
|
|
46
|
+
contextVariable: ['feature'],
|
|
47
|
+
},
|
|
48
|
+
stickWidth: {
|
|
49
|
+
type: 'number',
|
|
50
|
+
description: 'width in pixels of the lollipop stick',
|
|
51
|
+
defaultValue: 2,
|
|
52
|
+
contextVariable: ['feature'],
|
|
53
|
+
},
|
|
54
|
+
score: {
|
|
55
|
+
type: 'number',
|
|
56
|
+
description:
|
|
57
|
+
'the "score" of each lollipop, displayed as a number in the center of the circle',
|
|
58
|
+
defaultValue: `jexl:get(feature,'score')`,
|
|
59
|
+
contextVariable: ['feature'],
|
|
60
|
+
},
|
|
61
|
+
},
|
|
62
|
+
{ explicitlyTyped: true },
|
|
63
|
+
)
|