react-msaview 6.0.0 → 6.1.0
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/bundle/index.js +98 -98
- package/bundle/index.js.map +4 -4
- package/dist/columnCounts.d.ts +7 -0
- package/dist/columnCounts.js +18 -0
- package/dist/columnCounts.js.map +1 -1
- package/dist/components/DragHandle.d.ts +25 -0
- package/dist/components/DragHandle.js +49 -0
- package/dist/components/DragHandle.js.map +1 -0
- package/dist/components/ResizeHandles.d.ts +4 -2
- package/dist/components/ResizeHandles.js +29 -32
- package/dist/components/ResizeHandles.js.map +1 -1
- package/dist/components/VerticalScrollbar.js +2 -2
- package/dist/components/VerticalScrollbar.js.map +1 -1
- package/dist/components/dialogs/InterProScanDialog.js +52 -129
- package/dist/components/dialogs/InterProScanDialog.js.map +1 -1
- package/dist/components/header/settingsMenuItems.js +15 -0
- package/dist/components/header/settingsMenuItems.js.map +1 -1
- package/dist/components/minimap/Minimap.js +32 -4
- package/dist/components/minimap/Minimap.js.map +1 -1
- package/dist/components/msa/MSACanvas.js +8 -2
- package/dist/components/msa/MSACanvas.js.map +1 -1
- package/dist/components/msa/MSACanvasBlock.js +12 -3
- package/dist/components/msa/MSACanvasBlock.js.map +1 -1
- package/dist/components/msa/msaRaster.d.ts +63 -0
- package/dist/components/msa/msaRaster.js +252 -0
- package/dist/components/msa/msaRaster.js.map +1 -0
- package/dist/components/msa/renderMSABlock.d.ts +2 -1
- package/dist/components/msa/renderMSABlock.js +13 -21
- package/dist/components/msa/renderMSABlock.js.map +1 -1
- package/dist/components/msa/tileColor.d.ts +6 -0
- package/dist/components/msa/tileColor.js +35 -0
- package/dist/components/msa/tileColor.js.map +1 -0
- package/dist/components/renderCtx.d.ts +1 -1
- package/dist/components/tracks/TrackBlocks.d.ts +14 -0
- package/dist/components/tracks/TrackBlocks.js +65 -0
- package/dist/components/tracks/TrackBlocks.js.map +1 -0
- package/dist/components/tracks/{renderTracksSvg.d.ts → drawTracks.d.ts} +22 -10
- package/dist/components/tracks/drawTracks.js +193 -0
- package/dist/components/tracks/drawTracks.js.map +1 -0
- package/dist/components/tree/TreeCanvas.js +8 -2
- package/dist/components/tree/TreeCanvas.js.map +1 -1
- package/dist/components/tree/TreeCanvasBlock.js +7 -4
- package/dist/components/tree/TreeCanvasBlock.js.map +1 -1
- package/dist/components/tree/renderTreeCanvas.js +5 -2
- package/dist/components/tree/renderTreeCanvas.js.map +1 -1
- package/dist/constants.d.ts +3 -0
- package/dist/constants.js +8 -0
- package/dist/constants.js.map +1 -1
- package/dist/model/DataModel.d.ts +1 -4
- package/dist/model/DataModel.js +8 -10
- package/dist/model/DataModel.js.map +1 -1
- package/dist/model.d.ts +36 -7
- package/dist/model.js +113 -58
- package/dist/model.js.map +1 -1
- package/dist/neighborJoining.js +34 -18
- package/dist/neighborJoining.js.map +1 -1
- package/dist/renderTestEnv.d.ts +43 -0
- package/dist/renderTestEnv.js +88 -0
- package/dist/renderTestEnv.js.map +1 -0
- package/dist/renderToSvg.js +1 -1
- package/dist/renderToSvg.js.map +1 -1
- package/dist/sequenceLogo.d.ts +28 -0
- package/dist/sequenceLogo.js +47 -0
- package/dist/sequenceLogo.js.map +1 -0
- package/dist/types.d.ts +2 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +3 -3
- package/src/columnCounts.test.ts +15 -0
- package/src/columnCounts.ts +19 -0
- package/src/components/DragHandle.tsx +81 -0
- package/src/components/ResizeHandles.tsx +46 -54
- package/src/components/VerticalScrollbar.tsx +3 -2
- package/src/components/dialogs/InterProScanDialog.tsx +54 -131
- package/src/components/header/settingsMenuItems.ts +18 -0
- package/src/components/minimap/Minimap.tsx +47 -4
- package/src/components/msa/MSACanvas.tsx +30 -10
- package/src/components/msa/MSACanvasBlock.tsx +13 -3
- package/src/components/msa/msaRaster.test.ts +145 -0
- package/src/components/msa/msaRaster.ts +350 -0
- package/src/components/msa/renderMSABlock.ts +15 -20
- package/src/components/msa/tileColor.ts +37 -0
- package/src/components/renderCtx.ts +2 -0
- package/src/components/tracks/TrackBlocks.tsx +111 -0
- package/src/components/tracks/drawTracks.ts +323 -0
- package/src/components/tree/TreeCanvas.tsx +16 -4
- package/src/components/tree/TreeCanvasBlock.tsx +18 -11
- package/src/components/tree/renderTreeCanvas.ts +5 -1
- package/src/constants.ts +10 -0
- package/src/impgRender.test.tsx +2 -57
- package/src/largeTree.test.ts +16 -2
- package/src/model/DataModel.ts +11 -11
- package/src/model.ts +128 -60
- package/src/neighborJoining.ts +36 -18
- package/src/renderDomainsSvg.test.tsx +2 -57
- package/src/renderMinimapSvg.test.tsx +2 -56
- package/src/renderSequenceLogoSvg.test.tsx +176 -0
- package/src/renderTestEnv.ts +96 -0
- package/src/renderToSvg.tsx +1 -1
- package/src/sequenceLogo.test.ts +88 -0
- package/src/sequenceLogo.ts +63 -0
- package/src/types.ts +7 -1
- package/src/version.ts +1 -1
- package/dist/components/ConservationTrack.d.ts +0 -8
- package/dist/components/ConservationTrack.js +0 -49
- package/dist/components/ConservationTrack.js.map +0 -1
- package/dist/components/ScrollbarThumb.d.ts +0 -14
- package/dist/components/ScrollbarThumb.js +0 -28
- package/dist/components/ScrollbarThumb.js.map +0 -1
- package/dist/components/TextTrack.d.ts +0 -8
- package/dist/components/TextTrack.js +0 -58
- package/dist/components/TextTrack.js.map +0 -1
- package/dist/components/tracks/renderTracksSvg.js +0 -126
- package/dist/components/tracks/renderTracksSvg.js.map +0 -1
- package/src/components/ConservationTrack.tsx +0 -99
- package/src/components/ScrollbarThumb.tsx +0 -48
- package/src/components/TextTrack.tsx +0 -103
- package/src/components/tracks/renderTracksSvg.ts +0 -247
package/src/model.ts
CHANGED
|
@@ -18,8 +18,7 @@ import { calculateBlocks } from './calculateBlocks.ts'
|
|
|
18
18
|
import { clustalXColumnColors } from './clustalX.ts'
|
|
19
19
|
import colorSchemes from './colorSchemes.ts'
|
|
20
20
|
import { columnCountsFromRows, letterOfResidueSlot } from './columnCounts.ts'
|
|
21
|
-
import
|
|
22
|
-
import TextTrack from './components/TextTrack.tsx'
|
|
21
|
+
import TrackBlocks from './components/tracks/TrackBlocks.tsx'
|
|
23
22
|
import {
|
|
24
23
|
defaultAllowedGappyness,
|
|
25
24
|
defaultColWidth,
|
|
@@ -35,8 +34,11 @@ import {
|
|
|
35
34
|
defaultShowDomainLegend,
|
|
36
35
|
defaultShowDomains,
|
|
37
36
|
defaultSubFeatureRows,
|
|
37
|
+
labelReferenceFontSize,
|
|
38
38
|
maxCellSize,
|
|
39
39
|
minColWidth,
|
|
40
|
+
minLetterColWidth,
|
|
41
|
+
minLetterRowHeight,
|
|
40
42
|
minRowHeight,
|
|
41
43
|
segmentFeatureTypes,
|
|
42
44
|
segmentShades,
|
|
@@ -75,6 +77,7 @@ import {
|
|
|
75
77
|
visibleColsBefore,
|
|
76
78
|
} from './rowCoordinateCalculations.ts'
|
|
77
79
|
import { buildSeqPosIndex } from './seqPosToGlobalCol.ts'
|
|
80
|
+
import { maxBitsFor } from './sequenceLogo.ts'
|
|
78
81
|
import { stripDefault } from './stripDefault.ts'
|
|
79
82
|
import { computeRowInsertions, len, skipBlanks, transform } from './util.ts'
|
|
80
83
|
import { saveAs } from './vendor/fileSaver.ts'
|
|
@@ -99,6 +102,22 @@ function parseTreeText(text: string) {
|
|
|
99
102
|
return parseNewick(text.startsWith('SEQ') ? parseEmfTree(text).tree : text)
|
|
100
103
|
}
|
|
101
104
|
|
|
105
|
+
// Tracks that start hidden. The sequence logo answers a narrower question than
|
|
106
|
+
// conservation does and costs three times the vertical space, so it waits to be
|
|
107
|
+
// asked for.
|
|
108
|
+
const defaultOffTracks = new Set(['sequence-logo'])
|
|
109
|
+
|
|
110
|
+
// `turnedOffTracks` records the user's explicit choices only: an id is absent
|
|
111
|
+
// until they touch that track, and then its value is whether the track is OFF.
|
|
112
|
+
// Reading the default through this is what lets a track ship hidden without
|
|
113
|
+
// writing an entry into every snapshot and shared URL.
|
|
114
|
+
function trackIsOff(
|
|
115
|
+
turnedOffTracks: { get: (id: string) => boolean | undefined },
|
|
116
|
+
id: string,
|
|
117
|
+
) {
|
|
118
|
+
return turnedOffTracks.get(id) ?? defaultOffTracks.has(id)
|
|
119
|
+
}
|
|
120
|
+
|
|
102
121
|
/**
|
|
103
122
|
* #stateModel MsaView
|
|
104
123
|
*
|
|
@@ -249,7 +268,10 @@ function stateModelFactory() {
|
|
|
249
268
|
showOnly: types.maybe(types.string),
|
|
250
269
|
/**
|
|
251
270
|
* #property
|
|
252
|
-
*
|
|
271
|
+
* the user's explicit show/hide choice per track id, keyed by id with
|
|
272
|
+
* the value meaning "off". A track the user has never touched is absent
|
|
273
|
+
* and falls back to its own default (see `defaultOffTracks`), so a
|
|
274
|
+
* hidden-by-default track adds nothing to the shared URL.
|
|
253
275
|
*/
|
|
254
276
|
turnedOffTracks: stripDefault(types.map(types.boolean), {}),
|
|
255
277
|
|
|
@@ -375,6 +397,14 @@ function stateModelFactory() {
|
|
|
375
397
|
*/
|
|
376
398
|
conservationTrackHeight: 40,
|
|
377
399
|
|
|
400
|
+
/**
|
|
401
|
+
* #volatile
|
|
402
|
+
* taller than the conservation track by default: the logo spends its
|
|
403
|
+
* height on stacked glyphs, and a 40px stack of four residues leaves each
|
|
404
|
+
* one too short to identify
|
|
405
|
+
*/
|
|
406
|
+
sequenceLogoTrackHeight: 80,
|
|
407
|
+
|
|
378
408
|
/**
|
|
379
409
|
* #volatile
|
|
380
410
|
*/
|
|
@@ -1036,27 +1066,25 @@ function stateModelFactory() {
|
|
|
1036
1066
|
* Returns values 0-1 where 1 = fully conserved, 0 = no conservation.
|
|
1037
1067
|
*/
|
|
1038
1068
|
get conservation() {
|
|
1039
|
-
const { colStats,
|
|
1040
|
-
const alphabetSize = sequenceType === 'amino' ? 20 : 4
|
|
1041
|
-
const maxEntropy = Math.log2(alphabetSize)
|
|
1042
|
-
|
|
1069
|
+
const { colStats, alphabetMaxBits } = this
|
|
1043
1070
|
return Array.from({ length: colStats.numColumns }, (_, col) => {
|
|
1044
1071
|
const total = colStats.total(col)
|
|
1045
1072
|
const gapCount = colStats.gapCount(col)
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
colStats.forEachResidue(col, (_slot, count) => {
|
|
1051
|
-
const freq = count / nonGapTotal
|
|
1052
|
-
entropy -= freq * Math.log2(freq)
|
|
1053
|
-
})
|
|
1054
|
-
score =
|
|
1055
|
-
Math.max(0, 1 - entropy / maxEntropy) * (1 - gapCount / total)
|
|
1056
|
-
}
|
|
1057
|
-
return score
|
|
1073
|
+
return total > gapCount
|
|
1074
|
+
? Math.max(0, 1 - colStats.entropy(col) / alphabetMaxBits) *
|
|
1075
|
+
(1 - gapCount / total)
|
|
1076
|
+
: 0
|
|
1058
1077
|
})
|
|
1059
1078
|
},
|
|
1079
|
+
/**
|
|
1080
|
+
* #getter
|
|
1081
|
+
* The information content of a fully conserved column, in bits, which
|
|
1082
|
+
* depends on the alphabet. Both the entropy ceiling `conservation`
|
|
1083
|
+
* normalizes against and the y-axis ceiling of the sequence logo track.
|
|
1084
|
+
*/
|
|
1085
|
+
get alphabetMaxBits() {
|
|
1086
|
+
return maxBitsFor(this.sequenceType)
|
|
1087
|
+
},
|
|
1060
1088
|
/**
|
|
1061
1089
|
* #getter
|
|
1062
1090
|
* Per-column conservation of physicochemical property class (amino acids
|
|
@@ -1168,12 +1196,24 @@ function stateModelFactory() {
|
|
|
1168
1196
|
*/
|
|
1169
1197
|
get blocksY() {
|
|
1170
1198
|
return calculateBlocks({
|
|
1171
|
-
viewportSize:
|
|
1199
|
+
viewportSize: this.visibleMsaHeight,
|
|
1172
1200
|
viewportPos: -self.scrollY,
|
|
1173
1201
|
blockSize: self.blockSize,
|
|
1174
1202
|
mapSize: self.totalHeight,
|
|
1175
1203
|
})
|
|
1176
1204
|
},
|
|
1205
|
+
/**
|
|
1206
|
+
* #getter
|
|
1207
|
+
* height of the alignment viewport, px. The same subtraction as
|
|
1208
|
+
* msaAreaHeight, which is defined later in the views chain
|
|
1209
|
+
*/
|
|
1210
|
+
get visibleMsaHeight() {
|
|
1211
|
+
return (
|
|
1212
|
+
self.height -
|
|
1213
|
+
self.headerHeight -
|
|
1214
|
+
(self.msaAreaWidth < self.totalWidth ? self.minimapHeight : 0)
|
|
1215
|
+
)
|
|
1216
|
+
},
|
|
1177
1217
|
}))
|
|
1178
1218
|
.views(self => ({
|
|
1179
1219
|
/**
|
|
@@ -1200,15 +1240,10 @@ function stateModelFactory() {
|
|
|
1200
1240
|
/**
|
|
1201
1241
|
* #getter
|
|
1202
1242
|
* most-negative allowed scrollY, keeping the last row in view rather than
|
|
1203
|
-
* letting the whole alignment scroll off the top.
|
|
1204
|
-
* computed inline here because msaAreaHeight is defined later in the chain.
|
|
1243
|
+
* letting the whole alignment scroll off the top.
|
|
1205
1244
|
*/
|
|
1206
1245
|
get maxScrollY() {
|
|
1207
|
-
|
|
1208
|
-
self.height -
|
|
1209
|
-
self.headerHeight -
|
|
1210
|
-
(self.msaAreaWidth < self.totalWidth ? self.minimapHeight : 0)
|
|
1211
|
-
return Math.min(-self.totalHeight + visibleHeight, 0)
|
|
1246
|
+
return Math.min(-self.totalHeight + self.visibleMsaHeight, 0)
|
|
1212
1247
|
},
|
|
1213
1248
|
/**
|
|
1214
1249
|
* #getter
|
|
@@ -1216,7 +1251,8 @@ function stateModelFactory() {
|
|
|
1216
1251
|
get showMsaLetters() {
|
|
1217
1252
|
return (
|
|
1218
1253
|
self.drawMsaLetters &&
|
|
1219
|
-
self.rowHeight >=
|
|
1254
|
+
self.rowHeight >= minLetterRowHeight &&
|
|
1255
|
+
self.colWidth >= minLetterColWidth &&
|
|
1220
1256
|
self.colWidth > self.rowHeight / 2
|
|
1221
1257
|
)
|
|
1222
1258
|
},
|
|
@@ -1224,7 +1260,7 @@ function stateModelFactory() {
|
|
|
1224
1260
|
* #getter
|
|
1225
1261
|
*/
|
|
1226
1262
|
get showTreeText() {
|
|
1227
|
-
return self.drawLabels && self.rowHeight >=
|
|
1263
|
+
return self.drawLabels && self.rowHeight >= minLetterRowHeight
|
|
1228
1264
|
},
|
|
1229
1265
|
}))
|
|
1230
1266
|
.actions(self => ({
|
|
@@ -1437,11 +1473,9 @@ function stateModelFactory() {
|
|
|
1437
1473
|
* #action
|
|
1438
1474
|
*/
|
|
1439
1475
|
toggleTrack(id: string) {
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
self.turnedOffTracks.set(id, true)
|
|
1444
|
-
}
|
|
1476
|
+
// the stored value is "is off", so the current shown state is exactly
|
|
1477
|
+
// what the flipped entry should hold
|
|
1478
|
+
self.turnedOffTracks.set(id, !trackIsOff(self.turnedOffTracks, id))
|
|
1445
1479
|
},
|
|
1446
1480
|
/**
|
|
1447
1481
|
* #action
|
|
@@ -1455,10 +1489,13 @@ function stateModelFactory() {
|
|
|
1455
1489
|
* #getter
|
|
1456
1490
|
*/
|
|
1457
1491
|
get labelWidthMap() {
|
|
1458
|
-
const { showTreeText, leaves, treeMetadata
|
|
1492
|
+
const { showTreeText, leaves, treeMetadata } = self
|
|
1459
1493
|
// gated on the same condition the renderer draws labels under, so the
|
|
1460
1494
|
// gutter labelsWidth reserves and the labels actually drawn cannot
|
|
1461
|
-
// disagree -- and so turning labels off hands their space to the tree
|
|
1495
|
+
// disagree -- and so turning labels off hands their space to the tree.
|
|
1496
|
+
// Measured at a fixed reference size and scaled by labelWidthScale:
|
|
1497
|
+
// re-measuring every leaf on every vertical-zoom frame cost ~200ms on a
|
|
1498
|
+
// 50k-leaf tree
|
|
1462
1499
|
return showTreeText
|
|
1463
1500
|
? new Map(
|
|
1464
1501
|
leaves.map(node => {
|
|
@@ -1467,12 +1504,24 @@ function stateModelFactory() {
|
|
|
1467
1504
|
// to the row name, and measuring '' would size the gutter (and
|
|
1468
1505
|
// the label's click target) to nothing
|
|
1469
1506
|
const displayName = treeMetadata[name]?.genome || name
|
|
1470
|
-
return [
|
|
1507
|
+
return [
|
|
1508
|
+
name,
|
|
1509
|
+
measureTextCanvas(displayName, labelReferenceFontSize),
|
|
1510
|
+
] as const
|
|
1471
1511
|
}),
|
|
1472
1512
|
)
|
|
1473
1513
|
: new Map<string, number>()
|
|
1474
1514
|
},
|
|
1475
1515
|
|
|
1516
|
+
/**
|
|
1517
|
+
* #getter
|
|
1518
|
+
* factor turning a labelWidthMap entry into its width at the current
|
|
1519
|
+
* font size
|
|
1520
|
+
*/
|
|
1521
|
+
get labelWidthScale() {
|
|
1522
|
+
return self.fontSize / labelReferenceFontSize
|
|
1523
|
+
},
|
|
1524
|
+
|
|
1476
1525
|
get labelsWidth() {
|
|
1477
1526
|
// a loop, not Math.max(...widths.values()): spreading a map of every
|
|
1478
1527
|
// leaf passes one argument per row, and the argument limit is somewhere
|
|
@@ -1485,7 +1534,7 @@ function stateModelFactory() {
|
|
|
1485
1534
|
max = width
|
|
1486
1535
|
}
|
|
1487
1536
|
}
|
|
1488
|
-
return max
|
|
1537
|
+
return max * this.labelWidthScale
|
|
1489
1538
|
},
|
|
1490
1539
|
|
|
1491
1540
|
/**
|
|
@@ -1513,10 +1562,11 @@ function stateModelFactory() {
|
|
|
1513
1562
|
.map(t => ({
|
|
1514
1563
|
model: {
|
|
1515
1564
|
...t,
|
|
1565
|
+
kind: 'text' as const,
|
|
1516
1566
|
data: hideGapsEffective ? skipBlanks(blanks, t.data!) : t.data,
|
|
1517
1567
|
height: rowHeight,
|
|
1518
1568
|
},
|
|
1519
|
-
ReactComponent:
|
|
1569
|
+
ReactComponent: TrackBlocks,
|
|
1520
1570
|
}))
|
|
1521
1571
|
},
|
|
1522
1572
|
|
|
@@ -1524,28 +1574,35 @@ function stateModelFactory() {
|
|
|
1524
1574
|
* #getter
|
|
1525
1575
|
*/
|
|
1526
1576
|
get tracks(): BasicTrack[] {
|
|
1527
|
-
const conservationTrack
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
},
|
|
1534
|
-
ReactComponent: ConservationTrack,
|
|
1577
|
+
const conservationTrack = {
|
|
1578
|
+
id: 'conservation',
|
|
1579
|
+
name: 'Conservation',
|
|
1580
|
+
kind: 'bar' as const,
|
|
1581
|
+
height: self.conservationTrackHeight,
|
|
1582
|
+
barColor: 'gray',
|
|
1535
1583
|
}
|
|
1536
|
-
const propertyConservationTrack
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1584
|
+
const propertyConservationTrack = {
|
|
1585
|
+
id: 'property-conservation',
|
|
1586
|
+
name: 'Property conservation',
|
|
1587
|
+
kind: 'bar' as const,
|
|
1588
|
+
height: self.conservationTrackHeight,
|
|
1589
|
+
barColor: '#6a51a3',
|
|
1590
|
+
}
|
|
1591
|
+
const sequenceLogoTrack = {
|
|
1592
|
+
id: 'sequence-logo',
|
|
1593
|
+
name: 'Sequence logo',
|
|
1594
|
+
kind: 'logo' as const,
|
|
1595
|
+
height: self.sequenceLogoTrackHeight,
|
|
1544
1596
|
}
|
|
1545
1597
|
return [
|
|
1546
1598
|
...this.adapterTrackModels,
|
|
1547
|
-
|
|
1548
|
-
|
|
1599
|
+
...[
|
|
1600
|
+
conservationTrack,
|
|
1601
|
+
...(self.sequenceType === 'amino'
|
|
1602
|
+
? [propertyConservationTrack]
|
|
1603
|
+
: []),
|
|
1604
|
+
sequenceLogoTrack,
|
|
1605
|
+
].map(model => ({ model, ReactComponent: TrackBlocks })),
|
|
1549
1606
|
]
|
|
1550
1607
|
},
|
|
1551
1608
|
|
|
@@ -1553,7 +1610,9 @@ function stateModelFactory() {
|
|
|
1553
1610
|
* #getter
|
|
1554
1611
|
*/
|
|
1555
1612
|
get turnedOnTracks() {
|
|
1556
|
-
return this.tracks.filter(
|
|
1613
|
+
return this.tracks.filter(
|
|
1614
|
+
f => !trackIsOff(self.turnedOffTracks, f.model.id),
|
|
1615
|
+
)
|
|
1557
1616
|
},
|
|
1558
1617
|
|
|
1559
1618
|
/**
|
|
@@ -1810,11 +1869,11 @@ function stateModelFactory() {
|
|
|
1810
1869
|
* needs to keep residues readable on top of the boxes.
|
|
1811
1870
|
*/
|
|
1812
1871
|
get domainBands() {
|
|
1872
|
+
const { blanks } = self
|
|
1813
1873
|
const bands = new Map<string, DomainBand[]>()
|
|
1814
1874
|
for (const [name, annotations] of Object.entries(
|
|
1815
1875
|
self.annotationsByRow,
|
|
1816
1876
|
)) {
|
|
1817
|
-
const { blanks } = self
|
|
1818
1877
|
const rowBands = annotations
|
|
1819
1878
|
.toSorted((a, b) => len(b) - len(a))
|
|
1820
1879
|
.map(annotation => {
|
|
@@ -1942,8 +2001,11 @@ function stateModelFactory() {
|
|
|
1942
2001
|
return undefined
|
|
1943
2002
|
}
|
|
1944
2003
|
const { colStats } = self
|
|
2004
|
+
if (mouseCol >= colStats.numColumns) {
|
|
2005
|
+
return undefined
|
|
2006
|
+
}
|
|
1945
2007
|
const total = colStats.total(mouseCol)
|
|
1946
|
-
if (
|
|
2008
|
+
if (!total) {
|
|
1947
2009
|
return undefined
|
|
1948
2010
|
}
|
|
1949
2011
|
const gaps = colStats.gapCount(mouseCol)
|
|
@@ -1988,6 +2050,12 @@ function stateModelFactory() {
|
|
|
1988
2050
|
setConservationTrackHeight(arg: number) {
|
|
1989
2051
|
self.conservationTrackHeight = arg
|
|
1990
2052
|
},
|
|
2053
|
+
/**
|
|
2054
|
+
* #action
|
|
2055
|
+
*/
|
|
2056
|
+
setSequenceLogoTrackHeight(arg: number) {
|
|
2057
|
+
self.sequenceLogoTrackHeight = arg
|
|
2058
|
+
},
|
|
1991
2059
|
/**
|
|
1992
2060
|
* #action
|
|
1993
2061
|
*/
|
package/src/neighborJoining.ts
CHANGED
|
@@ -31,25 +31,43 @@ const BLOSUM62_DATA = [
|
|
|
31
31
|
[-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4, 1],
|
|
32
32
|
]
|
|
33
33
|
|
|
34
|
-
const
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
34
|
+
const NUM_SYMBOLS = BLOSUM62_KEYS.length
|
|
35
|
+
const BLOSUM62 = Int8Array.from(BLOSUM62_DATA.flat())
|
|
36
|
+
|
|
37
|
+
// charCode -> row/column in BLOSUM62, -1 for anything the matrix does not name.
|
|
38
|
+
// A flat table rather than a Map of Maps keyed by an upper-cased character, for
|
|
39
|
+
// the reason columnCounts.ts gives: the distance matrix is O(rows^2 * columns)
|
|
40
|
+
// lookups, and on a 120x800 alignment the Map form spent nearly all of its time
|
|
41
|
+
// allocating one-character strings in toUpperCase: 2.6s to build a tree, versus
|
|
42
|
+
// 0.34s here for a byte-identical newick.
|
|
43
|
+
const symbolOfCode = new Int8Array(128).fill(-1)
|
|
44
|
+
for (let i = 0; i < NUM_SYMBOLS; i++) {
|
|
45
|
+
const key = BLOSUM62_KEYS[i]!
|
|
46
|
+
symbolOfCode[key.charCodeAt(0)] = i
|
|
47
|
+
symbolOfCode[key.toLowerCase().charCodeAt(0)] = i
|
|
41
48
|
}
|
|
42
49
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
}
|
|
50
|
+
// gap, or past the end of a row shorter than the alignment
|
|
51
|
+
const GAP = -2
|
|
46
52
|
|
|
47
53
|
// stockholm and a3m write gaps as '.', not just '-'. Scoring a '.' as a residue
|
|
48
54
|
// charges the pair the unknown-symbol penalty on every padded column, so two
|
|
49
55
|
// sequences that differ only in where an unrelated row forced padding come out
|
|
50
56
|
// as divergent.
|
|
51
|
-
function
|
|
52
|
-
|
|
57
|
+
function symbolAt(seq: string, i: number) {
|
|
58
|
+
if (i >= seq.length) {
|
|
59
|
+
return GAP
|
|
60
|
+
}
|
|
61
|
+
const code = seq.charCodeAt(i)
|
|
62
|
+
// bit trick: (code - 45) >>> 0 <= 1 checks for '-' (45) or '.' (46)
|
|
63
|
+
if ((code - 45) >>> 0 <= 1) {
|
|
64
|
+
return GAP
|
|
65
|
+
}
|
|
66
|
+
return code < 128 ? symbolOfCode[code]! : -1
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function blosumScore(a: number, b: number) {
|
|
70
|
+
return a < 0 || b < 0 ? -4 : BLOSUM62[a * NUM_SYMBOLS + b]!
|
|
53
71
|
}
|
|
54
72
|
|
|
55
73
|
function computePairwiseDistance(seq1: string, seq2: string) {
|
|
@@ -63,11 +81,11 @@ function computePairwiseDistance(seq1: string, seq2: string) {
|
|
|
63
81
|
let maxPossibleScore = 0
|
|
64
82
|
|
|
65
83
|
for (let i = 0; i < len; i++) {
|
|
66
|
-
const a = seq1
|
|
67
|
-
const b = seq2
|
|
84
|
+
const a = symbolAt(seq1, i)
|
|
85
|
+
const b = symbolAt(seq2, i)
|
|
68
86
|
|
|
69
|
-
const aGap =
|
|
70
|
-
const bGap =
|
|
87
|
+
const aGap = a === GAP
|
|
88
|
+
const bGap = b === GAP
|
|
71
89
|
if (aGap && bGap) {
|
|
72
90
|
continue
|
|
73
91
|
}
|
|
@@ -77,8 +95,8 @@ function computePairwiseDistance(seq1: string, seq2: string) {
|
|
|
77
95
|
continue
|
|
78
96
|
}
|
|
79
97
|
|
|
80
|
-
totalScore +=
|
|
81
|
-
maxPossibleScore += Math.max(
|
|
98
|
+
totalScore += blosumScore(a, b)
|
|
99
|
+
maxPossibleScore += Math.max(blosumScore(a, a), blosumScore(b, b))
|
|
82
100
|
}
|
|
83
101
|
|
|
84
102
|
if (compared === 0) {
|
|
@@ -9,67 +9,12 @@ import { enableStaticRendering } from 'mobx-react'
|
|
|
9
9
|
import { beforeAll, expect, test } from 'vitest'
|
|
10
10
|
|
|
11
11
|
import MSAModelF from './model.ts'
|
|
12
|
+
import { installRenderTestEnv } from './renderTestEnv.ts'
|
|
12
13
|
import { renderToSvg } from './renderToSvg.tsx'
|
|
13
14
|
|
|
14
|
-
class Mat {
|
|
15
|
-
constructor(
|
|
16
|
-
public a = 1,
|
|
17
|
-
public b = 0,
|
|
18
|
-
public c = 0,
|
|
19
|
-
public d = 1,
|
|
20
|
-
public e = 0,
|
|
21
|
-
public f = 0,
|
|
22
|
-
) {}
|
|
23
|
-
multiply(o: Mat) {
|
|
24
|
-
return new Mat(
|
|
25
|
-
this.a * o.a + this.c * o.b,
|
|
26
|
-
this.b * o.a + this.d * o.b,
|
|
27
|
-
this.a * o.c + this.c * o.d,
|
|
28
|
-
this.b * o.c + this.d * o.d,
|
|
29
|
-
this.a * o.e + this.c * o.f + this.e,
|
|
30
|
-
this.b * o.e + this.d * o.f + this.f,
|
|
31
|
-
)
|
|
32
|
-
}
|
|
33
|
-
translate(x: number, y = 0) {
|
|
34
|
-
return this.multiply(new Mat(1, 0, 0, 1, x, y))
|
|
35
|
-
}
|
|
36
|
-
scale(x: number, y = x) {
|
|
37
|
-
return this.multiply(new Mat(x, 0, 0, y, 0, 0))
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
class Pt {
|
|
41
|
-
constructor(
|
|
42
|
-
public x = 0,
|
|
43
|
-
public y = 0,
|
|
44
|
-
) {}
|
|
45
|
-
matrixTransform(m: Mat) {
|
|
46
|
-
return new Pt(
|
|
47
|
-
m.a * this.x + m.c * this.y + m.e,
|
|
48
|
-
m.b * this.x + m.d * this.y + m.f,
|
|
49
|
-
)
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
|
|
53
15
|
beforeAll(() => {
|
|
54
16
|
enableStaticRendering(true)
|
|
55
|
-
|
|
56
|
-
g.DOMMatrix = Mat
|
|
57
|
-
g.DOMPoint = Pt
|
|
58
|
-
HTMLCanvasElement.prototype.getContext = function () {
|
|
59
|
-
let font = '10px sans-serif'
|
|
60
|
-
return {
|
|
61
|
-
get font() {
|
|
62
|
-
return font
|
|
63
|
-
},
|
|
64
|
-
set font(v: string) {
|
|
65
|
-
font = v
|
|
66
|
-
},
|
|
67
|
-
measureText(t: string) {
|
|
68
|
-
const size = Number.parseFloat(font) || 10
|
|
69
|
-
return { width: t.length * size * 0.6 } as TextMetrics
|
|
70
|
-
},
|
|
71
|
-
} as unknown as CanvasRenderingContext2D
|
|
72
|
-
} as unknown as typeof HTMLCanvasElement.prototype.getContext
|
|
17
|
+
installRenderTestEnv()
|
|
73
18
|
})
|
|
74
19
|
|
|
75
20
|
const msa = `>seq1
|
|
@@ -9,66 +9,12 @@ import { enableStaticRendering } from 'mobx-react'
|
|
|
9
9
|
import { beforeAll, expect, test } from 'vitest'
|
|
10
10
|
|
|
11
11
|
import MSAModelF from './model.ts'
|
|
12
|
+
import { installRenderTestEnv } from './renderTestEnv.ts'
|
|
12
13
|
import { renderToSvg } from './renderToSvg.tsx'
|
|
13
14
|
|
|
14
|
-
class Mat {
|
|
15
|
-
constructor(
|
|
16
|
-
public a = 1,
|
|
17
|
-
public b = 0,
|
|
18
|
-
public c = 0,
|
|
19
|
-
public d = 1,
|
|
20
|
-
public e = 0,
|
|
21
|
-
public f = 0,
|
|
22
|
-
) {}
|
|
23
|
-
multiply(o: Mat) {
|
|
24
|
-
return new Mat(
|
|
25
|
-
this.a * o.a + this.c * o.b,
|
|
26
|
-
this.b * o.a + this.d * o.b,
|
|
27
|
-
this.a * o.c + this.c * o.d,
|
|
28
|
-
this.b * o.c + this.d * o.d,
|
|
29
|
-
this.a * o.e + this.c * o.f + this.e,
|
|
30
|
-
this.b * o.e + this.d * o.f + this.f,
|
|
31
|
-
)
|
|
32
|
-
}
|
|
33
|
-
translate(x: number, y = 0) {
|
|
34
|
-
return this.multiply(new Mat(1, 0, 0, 1, x, y))
|
|
35
|
-
}
|
|
36
|
-
scale(x: number, y = x) {
|
|
37
|
-
return this.multiply(new Mat(x, 0, 0, y, 0, 0))
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
class Pt {
|
|
41
|
-
constructor(
|
|
42
|
-
public x = 0,
|
|
43
|
-
public y = 0,
|
|
44
|
-
) {}
|
|
45
|
-
matrixTransform(m: Mat) {
|
|
46
|
-
return new Pt(
|
|
47
|
-
m.a * this.x + m.c * this.y + m.e,
|
|
48
|
-
m.b * this.x + m.d * this.y + m.f,
|
|
49
|
-
)
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
|
|
53
15
|
beforeAll(() => {
|
|
54
16
|
enableStaticRendering(true)
|
|
55
|
-
|
|
56
|
-
g.DOMMatrix = Mat
|
|
57
|
-
g.DOMPoint = Pt
|
|
58
|
-
HTMLCanvasElement.prototype.getContext = function () {
|
|
59
|
-
let font = '10px sans-serif'
|
|
60
|
-
return {
|
|
61
|
-
get font() {
|
|
62
|
-
return font
|
|
63
|
-
},
|
|
64
|
-
set font(v: string) {
|
|
65
|
-
font = v
|
|
66
|
-
},
|
|
67
|
-
measureText: (t: string) => ({
|
|
68
|
-
width: t.length * (Number.parseFloat(font) || 10) * 0.6,
|
|
69
|
-
}),
|
|
70
|
-
} as unknown as CanvasRenderingContext2D
|
|
71
|
-
} as unknown as typeof HTMLCanvasElement.prototype.getContext
|
|
17
|
+
installRenderTestEnv()
|
|
72
18
|
})
|
|
73
19
|
|
|
74
20
|
const width = 1000
|