react-msaview 5.1.1 → 5.2.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/README.md +41 -8
- package/bundle/index.js +17 -17
- package/bundle/index.js.LICENSE.txt +1 -1
- package/bundle/index.js.map +1 -1
- package/dist/calculateBlocks.d.ts +4 -10
- package/dist/calculateBlocks.js +12 -20
- package/dist/calculateBlocks.js.map +1 -1
- package/dist/components/ConservationTrack.js +20 -37
- package/dist/components/ConservationTrack.js.map +1 -1
- package/dist/components/MSAView.js +8 -1
- package/dist/components/MSAView.js.map +1 -1
- package/dist/components/ResizeHandles.d.ts +3 -0
- package/dist/components/ResizeHandles.js +60 -105
- package/dist/components/ResizeHandles.js.map +1 -1
- package/dist/components/TextTrack.js +25 -34
- package/dist/components/TextTrack.js.map +1 -1
- package/dist/components/Track.js +1 -1
- package/dist/components/Track.js.map +1 -1
- package/dist/components/VerticalScrollbar.js +2 -2
- package/dist/components/VerticalScrollbar.js.map +1 -1
- package/dist/components/dialogs/ExportSVGDialog.js +20 -20
- package/dist/components/dialogs/ExportSVGDialog.js.map +1 -1
- package/dist/components/dialogs/InterProScanDialog.js +6 -1
- package/dist/components/dialogs/InterProScanDialog.js.map +1 -1
- package/dist/components/dialogs/UserProvidedDomainsDialog.js +1 -2
- package/dist/components/dialogs/UserProvidedDomainsDialog.js.map +1 -1
- package/dist/components/header/ColorSchemeMenu.js +1 -1
- package/dist/components/header/ColorSchemeMenu.js.map +1 -1
- package/dist/components/header/FileMenu.js +3 -2
- package/dist/components/header/FileMenu.js.map +1 -1
- package/dist/components/header/Header.js +1 -1
- package/dist/components/header/HeaderInfoArea.js +28 -8
- package/dist/components/header/HeaderInfoArea.js.map +1 -1
- package/dist/components/header/MSASettingsMenu.js +1 -1
- package/dist/components/header/MSASettingsMenu.js.map +1 -1
- package/dist/components/header/TreeSettingsMenu.js +1 -1
- package/dist/components/header/TreeSettingsMenu.js.map +1 -1
- package/dist/components/header/ZoomControls.js +7 -2
- package/dist/components/header/ZoomControls.js.map +1 -1
- package/dist/components/header/ZoomMenu.js +9 -0
- package/dist/components/header/ZoomMenu.js.map +1 -1
- package/dist/components/header/settingsMenuItems.js +24 -8
- package/dist/components/header/settingsMenuItems.js.map +1 -1
- package/dist/components/minimap/Minimap.js +6 -17
- package/dist/components/minimap/Minimap.js.map +1 -1
- package/dist/components/minimap/MinimapSVG.js +7 -17
- package/dist/components/minimap/MinimapSVG.js.map +1 -1
- package/dist/components/minimap/minimapLayout.d.ts +9 -0
- package/dist/components/minimap/minimapLayout.js +15 -0
- package/dist/components/minimap/minimapLayout.js.map +1 -0
- package/dist/components/msa/MSACanvas.js +6 -1
- package/dist/components/msa/MSACanvas.js.map +1 -1
- package/dist/components/msa/MSACanvasBlock.js +5 -1
- package/dist/components/msa/MSACanvasBlock.js.map +1 -1
- package/dist/components/msa/MSAMouseoverCanvas.js +2 -2
- package/dist/components/msa/MSAMouseoverCanvas.js.map +1 -1
- package/dist/components/msa/getVisibleLeaves.d.ts +6 -0
- package/dist/components/msa/getVisibleLeaves.js +9 -0
- package/dist/components/msa/getVisibleLeaves.js.map +1 -0
- package/dist/components/msa/renderBoxFeatureCanvasBlock.d.ts +2 -1
- package/dist/components/msa/renderBoxFeatureCanvasBlock.js +5 -6
- package/dist/components/msa/renderBoxFeatureCanvasBlock.js.map +1 -1
- package/dist/components/msa/renderMSABlock.d.ts +2 -1
- package/dist/components/msa/renderMSABlock.js +100 -88
- package/dist/components/msa/renderMSABlock.js.map +1 -1
- package/dist/components/msa/renderMSAMouseover.js +5 -4
- package/dist/components/msa/renderMSAMouseover.js.map +1 -1
- package/dist/components/msa/visibleColRange.d.ts +8 -0
- package/dist/components/msa/visibleColRange.js +10 -0
- package/dist/components/msa/visibleColRange.js.map +1 -0
- package/dist/components/renderCtx.d.ts +7 -0
- package/dist/components/renderCtx.js +2 -0
- package/dist/components/renderCtx.js.map +1 -0
- package/dist/components/tracks/renderTracksSvg.d.ts +6 -5
- package/dist/components/tracks/renderTracksSvg.js +13 -5
- package/dist/components/tracks/renderTracksSvg.js.map +1 -1
- package/dist/components/tree/TreeCanvas.js.map +1 -1
- package/dist/components/tree/TreeCanvasBlock.js +2 -10
- package/dist/components/tree/TreeCanvasBlock.js.map +1 -1
- package/dist/components/tree/renderTreeCanvas.d.ts +5 -4
- package/dist/components/tree/renderTreeCanvas.js +18 -26
- package/dist/components/tree/renderTreeCanvas.js.map +1 -1
- package/dist/components/util.d.ts +0 -14
- package/dist/components/util.js +0 -58
- package/dist/components/util.js.map +1 -1
- package/dist/constants.d.ts +4 -0
- package/dist/constants.js +7 -0
- package/dist/constants.js.map +1 -1
- package/dist/flatToTree.js +6 -0
- package/dist/flatToTree.js.map +1 -1
- package/dist/hierarchy.d.ts +3 -1
- package/dist/hierarchy.js +142 -89
- package/dist/hierarchy.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +8 -0
- package/dist/index.js.map +1 -1
- package/dist/launchInterProScan.js +22 -9
- package/dist/launchInterProScan.js.map +1 -1
- package/dist/measureTextCanvas.js +2 -1
- package/dist/measureTextCanvas.js.map +1 -1
- package/dist/model.d.ts +234 -176
- package/dist/model.js +285 -259
- package/dist/model.js.map +1 -1
- package/dist/parseAsn1.js +8 -2
- package/dist/parseAsn1.js.map +1 -1
- package/dist/renderToSvg.d.ts +1 -1
- package/dist/renderToSvg.js +43 -46
- package/dist/renderToSvg.js.map +1 -1
- package/dist/rowCoordinateCalculations.d.ts +0 -9
- package/dist/rowCoordinateCalculations.js +0 -20
- package/dist/rowCoordinateCalculations.js.map +1 -1
- package/dist/setFontSize.d.ts +3 -0
- package/dist/setFontSize.js +14 -0
- package/dist/setFontSize.js.map +1 -0
- package/dist/useDevicePixelRatio.d.ts +1 -0
- package/dist/useDevicePixelRatio.js +27 -0
- package/dist/useDevicePixelRatio.js.map +1 -0
- package/dist/useWheelScroll.d.ts +3 -1
- package/dist/useWheelScroll.js +64 -22
- package/dist/useWheelScroll.js.map +1 -1
- package/dist/util.d.ts +5 -0
- package/dist/util.js +59 -1
- package/dist/util.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/webpack.js +0 -1
- package/dist/webpack.js.map +1 -1
- package/dist/wheelZoom.d.ts +4 -0
- package/dist/wheelZoom.js +43 -0
- package/dist/wheelZoom.js.map +1 -0
- package/package.json +5 -5
- package/src/calculateBlocks.ts +18 -51
- package/src/components/ConservationTrack.tsx +20 -45
- package/src/components/MSAView.tsx +10 -1
- package/src/components/ResizeHandles.tsx +101 -113
- package/src/components/TextTrack.tsx +31 -38
- package/src/components/Track.tsx +1 -1
- package/src/components/VerticalScrollbar.tsx +2 -2
- package/src/components/dialogs/ExportSVGDialog.tsx +27 -17
- package/src/components/dialogs/InterProScanDialog.tsx +6 -1
- package/src/components/dialogs/UserProvidedDomainsDialog.tsx +1 -2
- package/src/components/header/ColorSchemeMenu.tsx +1 -0
- package/src/components/header/FileMenu.tsx +3 -1
- package/src/components/header/Header.tsx +1 -1
- package/src/components/header/HeaderInfoArea.tsx +32 -7
- package/src/components/header/MSASettingsMenu.tsx +1 -0
- package/src/components/header/TreeSettingsMenu.tsx +1 -0
- package/src/components/header/ZoomControls.tsx +14 -1
- package/src/components/header/ZoomMenu.tsx +10 -0
- package/src/components/header/settingsMenuItems.ts +24 -8
- package/src/components/minimap/Minimap.tsx +6 -20
- package/src/components/minimap/MinimapSVG.tsx +10 -27
- package/src/components/minimap/minimapLayout.ts +17 -0
- package/src/components/msa/MSACanvas.tsx +9 -0
- package/src/components/msa/MSACanvasBlock.tsx +7 -1
- package/src/components/msa/MSAMouseoverCanvas.tsx +4 -3
- package/src/components/msa/getVisibleLeaves.ts +21 -0
- package/src/components/msa/renderBoxFeatureCanvasBlock.ts +9 -9
- package/src/components/msa/renderMSABlock.ts +123 -125
- package/src/components/msa/renderMSAMouseover.ts +5 -4
- package/src/components/msa/visibleColRange.ts +17 -0
- package/src/components/renderCtx.ts +29 -0
- package/src/components/tracks/renderTracksSvg.ts +20 -10
- package/src/components/tree/TreeCanvas.tsx +3 -1
- package/src/components/tree/TreeCanvasBlock.tsx +2 -12
- package/src/components/tree/renderTreeCanvas.ts +27 -30
- package/src/components/util.ts +0 -81
- package/src/constants.ts +8 -0
- package/src/flatToTree.test.ts +30 -0
- package/src/flatToTree.ts +10 -1
- package/src/hierarchy.test.ts +53 -1
- package/src/hierarchy.ts +153 -100
- package/src/index.ts +8 -0
- package/src/launchInterProScan.ts +33 -11
- package/src/measureTextCanvas.ts +3 -1
- package/src/model.ts +359 -306
- package/src/parseAsn1.test.ts +6 -0
- package/src/parseAsn1.ts +11 -3
- package/src/renderToSvg.tsx +99 -95
- package/src/rowCoordinateCalculations.test.ts +0 -51
- package/src/rowCoordinateCalculations.ts +0 -23
- package/src/setFontSize.test.ts +33 -0
- package/src/setFontSize.ts +20 -0
- package/src/useDevicePixelRatio.ts +29 -0
- package/src/useWheelScroll.ts +79 -21
- package/src/util.ts +60 -1
- package/src/version.ts +1 -1
- package/src/webpack.ts +0 -1
- package/src/wheelZoom.test.ts +46 -0
- package/src/wheelZoom.ts +47 -0
package/src/parseAsn1.test.ts
CHANGED
|
@@ -12,3 +12,9 @@ const r = readFileSync(
|
|
|
12
12
|
test('real data file', () => {
|
|
13
13
|
expect(parseAsn1(r)).toMatchSnapshot()
|
|
14
14
|
})
|
|
15
|
+
|
|
16
|
+
test('throws a clear error on input missing required sections', () => {
|
|
17
|
+
expect(() => parseAsn1('BioTreeContainer ::= { something {} }')).toThrow(
|
|
18
|
+
/missing/,
|
|
19
|
+
)
|
|
20
|
+
})
|
package/src/parseAsn1.ts
CHANGED
|
@@ -88,15 +88,23 @@ export function parseAsn1(
|
|
|
88
88
|
.replace(/^.*?::=/, '')
|
|
89
89
|
|
|
90
90
|
const sections = extractSections(normalized)
|
|
91
|
+
const { fdict, nodes } = sections
|
|
92
|
+
if (fdict === undefined || nodes === undefined) {
|
|
93
|
+
// a truncated/malformed BioTreeContainer would otherwise pass undefined to
|
|
94
|
+
// extractBracedBlocks and crash cryptically; fail with a clear message
|
|
95
|
+
throw new Error(
|
|
96
|
+
`invalid BioTreeContainer ASN.1: missing '${fdict === undefined ? 'fdict' : 'nodes'}' section`,
|
|
97
|
+
)
|
|
98
|
+
}
|
|
91
99
|
|
|
92
100
|
const dict = Object.fromEntries(
|
|
93
|
-
extractBracedBlocks(
|
|
101
|
+
extractBracedBlocks(fdict).flatMap(block => {
|
|
94
102
|
const entry = parseDictEntry(block)
|
|
95
103
|
return entry ? [[entry.id, remap[entry.name] || entry.name]] : []
|
|
96
104
|
}),
|
|
97
105
|
)
|
|
98
106
|
|
|
99
|
-
return extractBracedBlocks(
|
|
107
|
+
return extractBracedBlocks(nodes).flatMap(block => {
|
|
100
108
|
const node = parseNode(block)
|
|
101
109
|
if (!node) {
|
|
102
110
|
return []
|
|
@@ -108,7 +116,7 @@ export function parseAsn1(
|
|
|
108
116
|
...Object.fromEntries(
|
|
109
117
|
features.map(f => [dict[f.featureid], f.value] as const),
|
|
110
118
|
),
|
|
111
|
-
},
|
|
119
|
+
} as { id: number; parent: number; name: string },
|
|
112
120
|
]
|
|
113
121
|
})
|
|
114
122
|
}
|
package/src/renderToSvg.tsx
CHANGED
|
@@ -19,13 +19,23 @@ export interface ExportSvgOptions {
|
|
|
19
19
|
theme: Theme
|
|
20
20
|
includeMinimap?: boolean
|
|
21
21
|
includeTracks?: boolean
|
|
22
|
-
exportType:
|
|
22
|
+
exportType: 'entire' | 'viewport'
|
|
23
23
|
}
|
|
24
24
|
export async function renderToSvg(model: MsaViewModel, opts: ExportSvgOptions) {
|
|
25
|
-
await when(() =>
|
|
26
|
-
const {
|
|
27
|
-
|
|
25
|
+
await when(() => model.dataInitialized)
|
|
26
|
+
const {
|
|
27
|
+
width,
|
|
28
|
+
height,
|
|
29
|
+
scrollX,
|
|
30
|
+
scrollY,
|
|
31
|
+
totalTrackAreaHeight,
|
|
32
|
+
minimapHeight,
|
|
33
|
+
} = model
|
|
34
|
+
const { exportType, theme, includeTracks } = opts
|
|
28
35
|
const trackHeight = includeTracks ? totalTrackAreaHeight : 0
|
|
36
|
+
// the minimap reflects the live viewport scroll position, so it's only
|
|
37
|
+
// meaningful for a viewport export, never for the entire alignment
|
|
38
|
+
const includeMinimap = exportType === 'viewport' && !!opts.includeMinimap
|
|
29
39
|
|
|
30
40
|
if (exportType === 'entire') {
|
|
31
41
|
return render({
|
|
@@ -41,21 +51,18 @@ export async function renderToSvg(model: MsaViewModel, opts: ExportSvgOptions) {
|
|
|
41
51
|
includeTracks,
|
|
42
52
|
})
|
|
43
53
|
}
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
})
|
|
57
|
-
}
|
|
58
|
-
throw new Error('unknown export type')
|
|
54
|
+
return render({
|
|
55
|
+
width,
|
|
56
|
+
height: height + (includeMinimap ? minimapHeight : 0) + trackHeight,
|
|
57
|
+
contentHeight: height,
|
|
58
|
+
trackHeight,
|
|
59
|
+
theme,
|
|
60
|
+
model,
|
|
61
|
+
offsetY: -scrollY,
|
|
62
|
+
offsetX: -scrollX,
|
|
63
|
+
includeMinimap,
|
|
64
|
+
includeTracks,
|
|
65
|
+
})
|
|
59
66
|
}
|
|
60
67
|
|
|
61
68
|
async function render({
|
|
@@ -82,37 +89,48 @@ async function render({
|
|
|
82
89
|
includeTracks?: boolean
|
|
83
90
|
}) {
|
|
84
91
|
const { Context } = await import('@jbrowse/svgcanvas')
|
|
85
|
-
const
|
|
92
|
+
const { treeAreaWidth, minimapHeight } = model
|
|
93
|
+
|
|
94
|
+
const content = (
|
|
95
|
+
<>
|
|
96
|
+
{includeTracks && trackHeight > 0 ? (
|
|
97
|
+
<TrackRendering
|
|
98
|
+
Context={Context}
|
|
99
|
+
model={model}
|
|
100
|
+
theme={theme}
|
|
101
|
+
offsetX={offsetX}
|
|
102
|
+
msaAreaWidth={width - treeAreaWidth}
|
|
103
|
+
trackHeight={trackHeight}
|
|
104
|
+
/>
|
|
105
|
+
) : null}
|
|
106
|
+
<g
|
|
107
|
+
transform={trackHeight > 0 ? `translate(0 ${trackHeight})` : undefined}
|
|
108
|
+
>
|
|
109
|
+
<CoreRendering
|
|
110
|
+
Context={Context}
|
|
111
|
+
model={model}
|
|
112
|
+
theme={theme}
|
|
113
|
+
offsetX={offsetX}
|
|
114
|
+
offsetY={offsetY}
|
|
115
|
+
msaAreaWidth={width - treeAreaWidth}
|
|
116
|
+
contentHeight={contentHeight}
|
|
117
|
+
/>
|
|
118
|
+
</g>
|
|
119
|
+
</>
|
|
120
|
+
)
|
|
86
121
|
|
|
87
122
|
return renderToStaticMarkup(
|
|
88
123
|
<SvgWrapper width={width} height={height}>
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
<
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
) : null}
|
|
100
|
-
<g
|
|
101
|
-
transform={
|
|
102
|
-
trackHeight > 0 ? `translate(0 ${trackHeight})` : undefined
|
|
103
|
-
}
|
|
104
|
-
>
|
|
105
|
-
<CoreRendering
|
|
106
|
-
Context={Context}
|
|
107
|
-
model={model}
|
|
108
|
-
theme={theme}
|
|
109
|
-
offsetX={offsetX}
|
|
110
|
-
offsetY={offsetY}
|
|
111
|
-
width={width}
|
|
112
|
-
contentHeight={contentHeight}
|
|
113
|
-
/>
|
|
114
|
-
</g>
|
|
115
|
-
</Wrapper>
|
|
124
|
+
{includeMinimap ? (
|
|
125
|
+
<>
|
|
126
|
+
<g transform={`translate(${treeAreaWidth} 0)`}>
|
|
127
|
+
<MinimapSVG model={model} />
|
|
128
|
+
</g>
|
|
129
|
+
<g transform={`translate(0 ${minimapHeight})`}>{content}</g>
|
|
130
|
+
</>
|
|
131
|
+
) : (
|
|
132
|
+
content
|
|
133
|
+
)}
|
|
116
134
|
</SvgWrapper>,
|
|
117
135
|
)
|
|
118
136
|
}
|
|
@@ -120,7 +138,7 @@ async function render({
|
|
|
120
138
|
function CoreRendering({
|
|
121
139
|
model,
|
|
122
140
|
theme,
|
|
123
|
-
|
|
141
|
+
msaAreaWidth,
|
|
124
142
|
contentHeight,
|
|
125
143
|
offsetX,
|
|
126
144
|
offsetY,
|
|
@@ -128,7 +146,7 @@ function CoreRendering({
|
|
|
128
146
|
}: {
|
|
129
147
|
model: MsaViewModel
|
|
130
148
|
theme: Theme
|
|
131
|
-
|
|
149
|
+
msaAreaWidth: number
|
|
132
150
|
contentHeight: number
|
|
133
151
|
offsetX: number
|
|
134
152
|
offsetY: number
|
|
@@ -139,9 +157,8 @@ function CoreRendering({
|
|
|
139
157
|
const clipId2 = `msa-${id}`
|
|
140
158
|
const contrastScheme = colorContrast(colorScheme, theme)
|
|
141
159
|
|
|
142
|
-
const ctx1 = new Context(
|
|
143
|
-
|
|
144
|
-
const ctx2 = new Context(width, contentHeight) as any
|
|
160
|
+
const ctx1 = new Context(treeAreaWidth, contentHeight)
|
|
161
|
+
const ctx2 = new Context(msaAreaWidth, contentHeight)
|
|
145
162
|
renderBoxFeatureCanvasBlock({
|
|
146
163
|
ctx: ctx2,
|
|
147
164
|
offsetX,
|
|
@@ -150,7 +167,6 @@ function CoreRendering({
|
|
|
150
167
|
blockSizeYOverride: contentHeight,
|
|
151
168
|
highResScaleFactorOverride: 1,
|
|
152
169
|
})
|
|
153
|
-
const msaAreaWidth = width - treeAreaWidth
|
|
154
170
|
renderTreeCanvas({
|
|
155
171
|
model,
|
|
156
172
|
offsetY,
|
|
@@ -176,37 +192,52 @@ function CoreRendering({
|
|
|
176
192
|
<clipPath id={clipId1}>
|
|
177
193
|
<rect x={0} y={0} width={treeAreaWidth} height={contentHeight} />
|
|
178
194
|
</clipPath>
|
|
179
|
-
</defs>
|
|
180
|
-
<defs>
|
|
181
195
|
<clipPath id={clipId2}>
|
|
182
196
|
<rect x={0} y={0} width={msaAreaWidth} height={contentHeight} />
|
|
183
197
|
</clipPath>
|
|
184
198
|
</defs>
|
|
185
199
|
|
|
186
|
-
<
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
/>
|
|
190
|
-
<g
|
|
191
|
-
clipPath={`url(#${clipId2})`}
|
|
200
|
+
<ClipGroup clipId={clipId1} html={ctx1.getSvg().innerHTML} />
|
|
201
|
+
<ClipGroup
|
|
202
|
+
clipId={clipId2}
|
|
192
203
|
transform={`translate(${treeAreaWidth} 0)`}
|
|
193
|
-
|
|
204
|
+
html={ctx2.getSvg().innerHTML}
|
|
194
205
|
/>
|
|
195
206
|
</>
|
|
196
207
|
)
|
|
197
208
|
}
|
|
198
209
|
|
|
210
|
+
// Wraps SVG markup produced by a svgcanvas Context in a clipped group. The
|
|
211
|
+
// markup is injected verbatim since it's already serialized SVG, not React.
|
|
212
|
+
function ClipGroup({
|
|
213
|
+
clipId,
|
|
214
|
+
html,
|
|
215
|
+
transform,
|
|
216
|
+
}: {
|
|
217
|
+
clipId: string
|
|
218
|
+
html: string
|
|
219
|
+
transform?: string
|
|
220
|
+
}) {
|
|
221
|
+
return (
|
|
222
|
+
<g
|
|
223
|
+
clipPath={`url(#${clipId})`}
|
|
224
|
+
transform={transform}
|
|
225
|
+
dangerouslySetInnerHTML={{ __html: html }}
|
|
226
|
+
/>
|
|
227
|
+
)
|
|
228
|
+
}
|
|
229
|
+
|
|
199
230
|
function TrackRendering({
|
|
200
231
|
model,
|
|
201
232
|
theme,
|
|
202
|
-
|
|
233
|
+
msaAreaWidth,
|
|
203
234
|
trackHeight,
|
|
204
235
|
offsetX,
|
|
205
236
|
Context,
|
|
206
237
|
}: {
|
|
207
238
|
model: MsaViewModel
|
|
208
239
|
theme: Theme
|
|
209
|
-
|
|
240
|
+
msaAreaWidth: number
|
|
210
241
|
trackHeight: number
|
|
211
242
|
offsetX: number
|
|
212
243
|
Context: typeof ContextType
|
|
@@ -214,9 +245,8 @@ function TrackRendering({
|
|
|
214
245
|
const { treeAreaWidth, colorScheme, id } = model
|
|
215
246
|
const clipId = `tracks-${id}`
|
|
216
247
|
const contrastScheme = colorContrast(colorScheme, theme)
|
|
217
|
-
const msaAreaWidth = width - treeAreaWidth
|
|
218
248
|
|
|
219
|
-
const ctx = new Context(msaAreaWidth, trackHeight)
|
|
249
|
+
const ctx = new Context(msaAreaWidth, trackHeight)
|
|
220
250
|
|
|
221
251
|
renderAllTracks({
|
|
222
252
|
model,
|
|
@@ -234,34 +264,11 @@ function TrackRendering({
|
|
|
234
264
|
<rect x={0} y={0} width={msaAreaWidth} height={trackHeight} />
|
|
235
265
|
</clipPath>
|
|
236
266
|
</defs>
|
|
237
|
-
<
|
|
238
|
-
clipPath={`url(#${clipId})`}
|
|
239
|
-
dangerouslySetInnerHTML={{ __html: ctx.getSvg().innerHTML }}
|
|
240
|
-
/>
|
|
267
|
+
<ClipGroup clipId={clipId} html={ctx.getSvg().innerHTML} />
|
|
241
268
|
</g>
|
|
242
269
|
)
|
|
243
270
|
}
|
|
244
271
|
|
|
245
|
-
function MinimapWrapper({
|
|
246
|
-
model,
|
|
247
|
-
children,
|
|
248
|
-
}: {
|
|
249
|
-
model: MsaViewModel
|
|
250
|
-
children: React.ReactNode
|
|
251
|
-
}) {
|
|
252
|
-
const { minimapHeight, treeAreaWidth } = model
|
|
253
|
-
|
|
254
|
-
return (
|
|
255
|
-
<>
|
|
256
|
-
<g transform={`translate(${treeAreaWidth} 0)`}>
|
|
257
|
-
<MinimapSVG model={model} />
|
|
258
|
-
</g>
|
|
259
|
-
|
|
260
|
-
<g transform={`translate(0 ${minimapHeight})`}>{children}</g>
|
|
261
|
-
</>
|
|
262
|
-
)
|
|
263
|
-
}
|
|
264
|
-
|
|
265
272
|
function SvgWrapper({
|
|
266
273
|
width,
|
|
267
274
|
height,
|
|
@@ -277,13 +284,10 @@ function SvgWrapper({
|
|
|
277
284
|
height={height}
|
|
278
285
|
xmlns="http://www.w3.org/2000/svg"
|
|
279
286
|
xmlnsXlink="http://www.w3.org/1999/xlink"
|
|
280
|
-
viewBox={
|
|
287
|
+
viewBox={`0 0 ${width} ${height}`}
|
|
281
288
|
>
|
|
289
|
+
<rect width="100%" height="100%" fill="white" />
|
|
282
290
|
{children}
|
|
283
291
|
</svg>
|
|
284
292
|
)
|
|
285
293
|
}
|
|
286
|
-
|
|
287
|
-
function NullWrapper({ children }: { children: React.ReactNode }) {
|
|
288
|
-
return children
|
|
289
|
-
}
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { expect, test } from 'vitest'
|
|
2
2
|
|
|
3
3
|
import {
|
|
4
|
-
globalColToSeqPos,
|
|
5
4
|
globalColToVisibleCol,
|
|
6
5
|
visibleColToGlobalCol,
|
|
7
6
|
visibleColToSeqPos,
|
|
@@ -126,56 +125,6 @@ test('globalColToVisibleCol with consecutive blanks', () => {
|
|
|
126
125
|
expect(globalColToVisibleCol(blanks, 10)).toBe(5)
|
|
127
126
|
})
|
|
128
127
|
|
|
129
|
-
// Tests for globalColToSeqPos (global column → sequence position)
|
|
130
|
-
test('globalColToSeqPos with gaps in sequence', () => {
|
|
131
|
-
const sequence = 'AC-GT-A'
|
|
132
|
-
expect(globalColToSeqPos(sequence, 0)).toBe(0)
|
|
133
|
-
expect(globalColToSeqPos(sequence, 1)).toBe(1)
|
|
134
|
-
expect(globalColToSeqPos(sequence, 2)).toBe(2)
|
|
135
|
-
// Position 2 is a gap, so count before it is 2
|
|
136
|
-
expect(globalColToSeqPos(sequence, 3)).toBe(2)
|
|
137
|
-
expect(globalColToSeqPos(sequence, 4)).toBe(3)
|
|
138
|
-
expect(globalColToSeqPos(sequence, 5)).toBe(4)
|
|
139
|
-
// Position 5 is a gap, so count before it is 4
|
|
140
|
-
expect(globalColToSeqPos(sequence, 6)).toBe(4)
|
|
141
|
-
})
|
|
142
|
-
|
|
143
|
-
test('globalColToSeqPos with mixed gap characters (- and .)', () => {
|
|
144
|
-
const sequence = 'AC.GT-A'
|
|
145
|
-
expect(globalColToSeqPos(sequence, 0)).toBe(0)
|
|
146
|
-
expect(globalColToSeqPos(sequence, 1)).toBe(1)
|
|
147
|
-
// Position 2 is a gap (.), so count before it is 2
|
|
148
|
-
expect(globalColToSeqPos(sequence, 2)).toBe(2)
|
|
149
|
-
expect(globalColToSeqPos(sequence, 3)).toBe(2)
|
|
150
|
-
expect(globalColToSeqPos(sequence, 4)).toBe(3)
|
|
151
|
-
// Position 5 is a gap (-), so count before it is 4
|
|
152
|
-
expect(globalColToSeqPos(sequence, 5)).toBe(4)
|
|
153
|
-
expect(globalColToSeqPos(sequence, 6)).toBe(4)
|
|
154
|
-
})
|
|
155
|
-
|
|
156
|
-
test('globalColToSeqPos with no gaps in sequence', () => {
|
|
157
|
-
const sequence = 'ACGTA'
|
|
158
|
-
expect(globalColToSeqPos(sequence, 0)).toBe(0)
|
|
159
|
-
expect(globalColToSeqPos(sequence, 1)).toBe(1)
|
|
160
|
-
expect(globalColToSeqPos(sequence, 2)).toBe(2)
|
|
161
|
-
expect(globalColToSeqPos(sequence, 3)).toBe(3)
|
|
162
|
-
expect(globalColToSeqPos(sequence, 4)).toBe(4)
|
|
163
|
-
})
|
|
164
|
-
|
|
165
|
-
test('globalColToSeqPos with all gaps in sequence', () => {
|
|
166
|
-
const sequence = '-----'
|
|
167
|
-
expect(globalColToSeqPos(sequence, 0)).toBe(0)
|
|
168
|
-
expect(globalColToSeqPos(sequence, 1)).toBe(0)
|
|
169
|
-
expect(globalColToSeqPos(sequence, 2)).toBe(0)
|
|
170
|
-
expect(globalColToSeqPos(sequence, 3)).toBe(0)
|
|
171
|
-
expect(globalColToSeqPos(sequence, 4)).toBe(0)
|
|
172
|
-
})
|
|
173
|
-
|
|
174
|
-
test('globalColToSeqPos with position exceeding sequence length', () => {
|
|
175
|
-
const sequence = 'AC-GT'
|
|
176
|
-
expect(globalColToSeqPos(sequence, 10)).toBe(4)
|
|
177
|
-
})
|
|
178
|
-
|
|
179
128
|
// Tests for visibleColToSeqPos (visible column → sequence position)
|
|
180
129
|
test('visibleColToSeqPos returns sequence position or undefined for gaps', () => {
|
|
181
130
|
const seq = 'AC--GT--CT'
|
|
@@ -88,29 +88,6 @@ export function globalColToVisibleCol(
|
|
|
88
88
|
return globalCol - blanksBefore
|
|
89
89
|
}
|
|
90
90
|
|
|
91
|
-
/**
|
|
92
|
-
* Convert a global column index to a row-specific sequence position.
|
|
93
|
-
* This counts non-gap characters up to the given global column.
|
|
94
|
-
*
|
|
95
|
-
* @param seq - The row's sequence string (including gaps)
|
|
96
|
-
* @param globalCol - The global column index
|
|
97
|
-
* @returns The sequence position (count of non-gap characters before this column)
|
|
98
|
-
*/
|
|
99
|
-
export function globalColToSeqPos(seq: string, globalCol: number) {
|
|
100
|
-
let seqPos = 0
|
|
101
|
-
let currentCol = 0
|
|
102
|
-
const seqLen = seq.length
|
|
103
|
-
|
|
104
|
-
while (currentCol < globalCol && currentCol < seqLen) {
|
|
105
|
-
if (!isBlank(seq[currentCol])) {
|
|
106
|
-
seqPos++
|
|
107
|
-
}
|
|
108
|
-
currentCol++
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
return seqPos
|
|
112
|
-
}
|
|
113
|
-
|
|
114
91
|
/**
|
|
115
92
|
* Convert a visible column to a row-specific sequence position.
|
|
116
93
|
* Returns undefined if the position is a gap in the sequence.
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { describe, expect, test } from 'vitest'
|
|
2
|
+
|
|
3
|
+
import { setFontSize } from './setFontSize.ts'
|
|
4
|
+
|
|
5
|
+
describe('setFontSize', () => {
|
|
6
|
+
test('replaces integer size, preserving family', () => {
|
|
7
|
+
const ctx = { font: '10px sans-serif' }
|
|
8
|
+
setFontSize(ctx, 16)
|
|
9
|
+
expect(ctx.font).toBe('16px sans-serif')
|
|
10
|
+
})
|
|
11
|
+
|
|
12
|
+
test('handles fractional sizes from smooth zoom', () => {
|
|
13
|
+
// regression: \d+ alone matched "8604" inside "14.8604px" and produced an
|
|
14
|
+
// invalid string like "14 0.18px sans-serif", silently breaking fillText
|
|
15
|
+
const ctx = { font: '14.8604px sans-serif' }
|
|
16
|
+
setFontSize(ctx, 16.9372)
|
|
17
|
+
expect(ctx.font).toBe('16.9372px sans-serif')
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
test('does not accumulate "bold " across re-renders', () => {
|
|
21
|
+
const ctx = { font: '10px sans-serif' }
|
|
22
|
+
setFontSize(ctx, 12, true)
|
|
23
|
+
expect(ctx.font).toBe('bold 12px sans-serif')
|
|
24
|
+
setFontSize(ctx, 14, true)
|
|
25
|
+
expect(ctx.font).toBe('bold 14px sans-serif')
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
test('drops "bold " when bold goes false', () => {
|
|
29
|
+
const ctx = { font: 'bold 12px sans-serif' }
|
|
30
|
+
setFontSize(ctx, 14)
|
|
31
|
+
expect(ctx.font).toBe('14px sans-serif')
|
|
32
|
+
})
|
|
33
|
+
})
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
// Replace just the px size token in a canvas font string, preserving the
|
|
2
|
+
// family (and optionally toggling bold). Accepts anything with a `font` field,
|
|
3
|
+
// so it works for both a real CanvasRenderingContext2D and the svgcanvas
|
|
4
|
+
// export context.
|
|
5
|
+
//
|
|
6
|
+
// [\d.]+ is load-bearing: it matches fractional sizes produced by smooth zoom.
|
|
7
|
+
// \d+ alone matches only the fraction ("8604" in "14.8604px"), splicing the new
|
|
8
|
+
// size into the middle to make an invalid string like "14 0.18px sans-serif" —
|
|
9
|
+
// which canvas silently rejects, freezing the font and making fillText draw
|
|
10
|
+
// nothing while fillRect still works.
|
|
11
|
+
export function setFontSize(
|
|
12
|
+
ctx: { font: string },
|
|
13
|
+
fontSize: number,
|
|
14
|
+
bold = false,
|
|
15
|
+
) {
|
|
16
|
+
ctx.font = ctx.font.replace(
|
|
17
|
+
/(?:bold )?[\d.]+px/,
|
|
18
|
+
`${bold ? 'bold ' : ''}${fontSize}px`,
|
|
19
|
+
)
|
|
20
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { useSyncExternalStore } from 'react'
|
|
2
|
+
|
|
3
|
+
// Subscribes to devicePixelRatio changes (moving the window between monitors,
|
|
4
|
+
// browser zoom). The matchMedia query is pinned to the current ratio, so on each
|
|
5
|
+
// change we re-register against the new ratio to keep tracking subsequent moves.
|
|
6
|
+
function subscribe(callback: () => void) {
|
|
7
|
+
if (typeof window === 'undefined') {
|
|
8
|
+
return () => {}
|
|
9
|
+
}
|
|
10
|
+
let mql = window.matchMedia(`(resolution: ${window.devicePixelRatio}dppx)`)
|
|
11
|
+
function onChange() {
|
|
12
|
+
callback()
|
|
13
|
+
mql.removeEventListener('change', onChange)
|
|
14
|
+
mql = window.matchMedia(`(resolution: ${window.devicePixelRatio}dppx)`)
|
|
15
|
+
mql.addEventListener('change', onChange)
|
|
16
|
+
}
|
|
17
|
+
mql.addEventListener('change', onChange)
|
|
18
|
+
return () => {
|
|
19
|
+
mql.removeEventListener('change', onChange)
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function getSnapshot() {
|
|
24
|
+
return typeof window === 'undefined' ? 1 : window.devicePixelRatio
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function useDevicePixelRatio() {
|
|
28
|
+
return useSyncExternalStore(subscribe, getSnapshot, () => 1)
|
|
29
|
+
}
|
package/src/useWheelScroll.ts
CHANGED
|
@@ -1,17 +1,33 @@
|
|
|
1
1
|
import { useEffect, useRef, useState } from 'react'
|
|
2
2
|
|
|
3
|
+
import {
|
|
4
|
+
SCROLL_ZOOM_DIVISOR,
|
|
5
|
+
getZoomNormalizer,
|
|
6
|
+
normalizeWheelDelta,
|
|
7
|
+
zoomFactorFromAccum,
|
|
8
|
+
} from './wheelZoom.ts'
|
|
9
|
+
|
|
3
10
|
export function useWheelScroll({
|
|
4
11
|
ref,
|
|
5
12
|
onScrollX,
|
|
6
13
|
onScrollY,
|
|
14
|
+
onZoom,
|
|
15
|
+
scrollZoom,
|
|
7
16
|
}: {
|
|
8
17
|
ref: React.RefObject<HTMLDivElement | null>
|
|
9
18
|
onScrollX?: (delta: number) => void
|
|
10
19
|
onScrollY?: (delta: number) => void
|
|
20
|
+
onZoom?: (scaleFactor: number, offsetX: number, offsetY: number) => void
|
|
21
|
+
scrollZoom?: boolean
|
|
11
22
|
}) {
|
|
12
23
|
const scheduled = useRef(false)
|
|
24
|
+
const zoomScheduled = useRef(false)
|
|
13
25
|
const deltaX = useRef(0)
|
|
14
26
|
const deltaY = useRef(0)
|
|
27
|
+
const zoomAccum = useRef(0)
|
|
28
|
+
const zoomX = useRef(0)
|
|
29
|
+
const zoomY = useRef(0)
|
|
30
|
+
const lastZoomTime = useRef<number | null>(null)
|
|
15
31
|
const prevX = useRef(0)
|
|
16
32
|
const prevY = useRef(0)
|
|
17
33
|
const [mouseDragging, setMouseDragging] = useState(false)
|
|
@@ -22,35 +38,77 @@ export function useWheelScroll({
|
|
|
22
38
|
return
|
|
23
39
|
}
|
|
24
40
|
function onWheel(event: WheelEvent) {
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
41
|
+
// ctrlKey is also set by trackpad pinch gestures on all platforms. when
|
|
42
|
+
// scrollZoom is on, a plain vertical-dominant wheel zooms too; holding
|
|
43
|
+
// shift is the escape hatch to pan instead.
|
|
44
|
+
const isCtrlZoom = event.ctrlKey || event.metaKey
|
|
45
|
+
const isScrollZoom =
|
|
46
|
+
scrollZoom &&
|
|
47
|
+
!event.shiftKey &&
|
|
48
|
+
Math.abs(event.deltaY) >= Math.abs(event.deltaX)
|
|
49
|
+
if (onZoom && (isCtrlZoom || isScrollZoom)) {
|
|
50
|
+
const rect = curr!.getBoundingClientRect()
|
|
51
|
+
// normalize away deltaMode (line/page), then divide by an adaptive
|
|
52
|
+
// factor so fine pinches and coarse notches feel consistent
|
|
53
|
+
const dY = normalizeWheelDelta(event.deltaY, event.deltaMode)
|
|
54
|
+
const divisor = isCtrlZoom ? getZoomNormalizer(dY) : SCROLL_ZOOM_DIVISOR
|
|
55
|
+
zoomAccum.current += dY / divisor
|
|
56
|
+
zoomX.current = event.clientX - rect.left
|
|
57
|
+
zoomY.current = event.clientY - rect.top
|
|
58
|
+
if (!zoomScheduled.current) {
|
|
59
|
+
zoomScheduled.current = true
|
|
60
|
+
requestAnimationFrame(now => {
|
|
61
|
+
const elapsed =
|
|
62
|
+
lastZoomTime.current === null ? 16.67 : now - lastZoomTime.current
|
|
63
|
+
lastZoomTime.current = now
|
|
64
|
+
onZoom(
|
|
65
|
+
zoomFactorFromAccum(zoomAccum.current, Math.min(100, elapsed)),
|
|
66
|
+
zoomX.current,
|
|
67
|
+
zoomY.current,
|
|
68
|
+
)
|
|
69
|
+
zoomAccum.current = 0
|
|
70
|
+
zoomScheduled.current = false
|
|
71
|
+
})
|
|
72
|
+
}
|
|
73
|
+
event.preventDefault()
|
|
74
|
+
event.stopPropagation()
|
|
75
|
+
return
|
|
30
76
|
}
|
|
77
|
+
// only swallow the wheel event on an axis we actually handle, otherwise a
|
|
78
|
+
// vertical wheel over an x-only scroller (e.g. a Track) blocks page scroll
|
|
79
|
+
const consumeX = !!onScrollX && event.deltaX !== 0
|
|
80
|
+
const consumeY = !!onScrollY && event.deltaY !== 0
|
|
81
|
+
if (consumeX || consumeY) {
|
|
82
|
+
if (onScrollX) {
|
|
83
|
+
deltaX.current += event.deltaX
|
|
84
|
+
}
|
|
85
|
+
if (onScrollY) {
|
|
86
|
+
deltaY.current += event.deltaY
|
|
87
|
+
}
|
|
31
88
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
89
|
+
if (!scheduled.current) {
|
|
90
|
+
scheduled.current = true
|
|
91
|
+
requestAnimationFrame(() => {
|
|
92
|
+
if (onScrollX) {
|
|
93
|
+
onScrollX(-deltaX.current)
|
|
94
|
+
deltaX.current = 0
|
|
95
|
+
}
|
|
96
|
+
if (onScrollY) {
|
|
97
|
+
onScrollY(-deltaY.current)
|
|
98
|
+
deltaY.current = 0
|
|
99
|
+
}
|
|
100
|
+
scheduled.current = false
|
|
101
|
+
})
|
|
102
|
+
}
|
|
103
|
+
event.preventDefault()
|
|
104
|
+
event.stopPropagation()
|
|
45
105
|
}
|
|
46
|
-
event.preventDefault()
|
|
47
|
-
event.stopPropagation()
|
|
48
106
|
}
|
|
49
107
|
curr.addEventListener('wheel', onWheel, { passive: false })
|
|
50
108
|
return () => {
|
|
51
109
|
curr.removeEventListener('wheel', onWheel)
|
|
52
110
|
}
|
|
53
|
-
}, [ref, onScrollX, onScrollY])
|
|
111
|
+
}, [ref, onScrollX, onScrollY, onZoom, scrollZoom])
|
|
54
112
|
|
|
55
113
|
useEffect(() => {
|
|
56
114
|
if (!mouseDragging) {
|