react-msaview 4.4.6 → 4.6.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.
Files changed (122) hide show
  1. package/bundle/index.js +9 -9
  2. package/bundle/index.js.LICENSE.txt +8 -8
  3. package/bundle/index.js.map +1 -1
  4. package/dist/colorSchemes.d.ts +0 -6
  5. package/dist/colorSchemes.js +1 -119
  6. package/dist/colorSchemes.js.map +1 -1
  7. package/dist/components/ConservationTrack.d.ts +8 -0
  8. package/dist/components/ConservationTrack.js +54 -0
  9. package/dist/components/ConservationTrack.js.map +1 -0
  10. package/dist/components/Loading.js +14 -2
  11. package/dist/components/Loading.js.map +1 -1
  12. package/dist/components/MSAView.js +36 -0
  13. package/dist/components/MSAView.js.map +1 -1
  14. package/dist/components/SequenceTextArea.js +3 -2
  15. package/dist/components/SequenceTextArea.js.map +1 -1
  16. package/dist/components/TextTrack.d.ts +3 -3
  17. package/dist/components/TextTrack.js +4 -1
  18. package/dist/components/TextTrack.js.map +1 -1
  19. package/dist/components/Track.js +21 -8
  20. package/dist/components/Track.js.map +1 -1
  21. package/dist/components/dialogs/ExportSVGDialog.js +19 -3
  22. package/dist/components/dialogs/ExportSVGDialog.js.map +1 -1
  23. package/dist/components/header/GappynessSlider.d.ts +6 -0
  24. package/dist/components/header/GappynessSlider.js +19 -0
  25. package/dist/components/header/GappynessSlider.js.map +1 -0
  26. package/dist/components/header/Header.js +3 -1
  27. package/dist/components/header/Header.js.map +1 -1
  28. package/dist/components/header/HeaderMenu.js +30 -14
  29. package/dist/components/header/HeaderMenu.js.map +1 -1
  30. package/dist/components/minimap/MinimapSVG.js +4 -3
  31. package/dist/components/minimap/MinimapSVG.js.map +1 -1
  32. package/dist/components/msa/MSACanvasBlock.js +56 -42
  33. package/dist/components/msa/MSACanvasBlock.js.map +1 -1
  34. package/dist/components/msa/renderMSABlock.js +71 -26
  35. package/dist/components/msa/renderMSABlock.js.map +1 -1
  36. package/dist/components/msa/renderMSAMouseover.js +8 -1
  37. package/dist/components/msa/renderMSAMouseover.js.map +1 -1
  38. package/dist/components/tracks/renderTracksSvg.d.ts +29 -0
  39. package/dist/components/tracks/renderTracksSvg.js +83 -0
  40. package/dist/components/tracks/renderTracksSvg.js.map +1 -0
  41. package/dist/components/tree/TreeNodeMenu.js +2 -2
  42. package/dist/components/tree/TreeNodeMenu.js.map +1 -1
  43. package/dist/components/tree/renderTreeCanvas.d.ts +0 -1
  44. package/dist/components/tree/renderTreeCanvas.js +23 -24
  45. package/dist/components/tree/renderTreeCanvas.js.map +1 -1
  46. package/dist/constants.d.ts +22 -0
  47. package/dist/constants.js +26 -0
  48. package/dist/constants.js.map +1 -0
  49. package/dist/layout.js.map +1 -1
  50. package/dist/model/msaModel.js +3 -2
  51. package/dist/model/msaModel.js.map +1 -1
  52. package/dist/model/treeModel.js +9 -8
  53. package/dist/model/treeModel.js.map +1 -1
  54. package/dist/model.d.ts +271 -15
  55. package/dist/model.js +427 -128
  56. package/dist/model.js.map +1 -1
  57. package/dist/neighborJoining.d.ts +1 -0
  58. package/dist/neighborJoining.js +839 -0
  59. package/dist/neighborJoining.js.map +1 -0
  60. package/dist/neighborJoining.test.d.ts +1 -0
  61. package/dist/neighborJoining.test.js +110 -0
  62. package/dist/neighborJoining.test.js.map +1 -0
  63. package/dist/parsers/A3mMSA.d.ts +43 -0
  64. package/dist/parsers/A3mMSA.js +277 -0
  65. package/dist/parsers/A3mMSA.js.map +1 -0
  66. package/dist/parsers/A3mMSA.test.d.ts +1 -0
  67. package/dist/parsers/A3mMSA.test.js +138 -0
  68. package/dist/parsers/A3mMSA.test.js.map +1 -0
  69. package/dist/parsers/ClustalMSA.d.ts +4 -4
  70. package/dist/parsers/ClustalMSA.js +3 -1
  71. package/dist/parsers/ClustalMSA.js.map +1 -1
  72. package/dist/parsers/FastaMSA.js +17 -16
  73. package/dist/parsers/FastaMSA.js.map +1 -1
  74. package/dist/renderToSvg.d.ts +1 -0
  75. package/dist/renderToSvg.js +48 -18
  76. package/dist/renderToSvg.js.map +1 -1
  77. package/dist/rowCoordinateCalculations.js +2 -0
  78. package/dist/rowCoordinateCalculations.js.map +1 -1
  79. package/dist/types.d.ts +2 -3
  80. package/dist/util.js +17 -9
  81. package/dist/util.js.map +1 -1
  82. package/dist/version.d.ts +1 -1
  83. package/dist/version.js +1 -1
  84. package/package.json +6 -6
  85. package/src/colorSchemes.ts +1 -179
  86. package/src/components/ConservationTrack.tsx +104 -0
  87. package/src/components/Loading.tsx +44 -2
  88. package/src/components/MSAView.tsx +68 -0
  89. package/src/components/SequenceTextArea.tsx +3 -2
  90. package/src/components/TextTrack.tsx +7 -4
  91. package/src/components/Track.tsx +25 -9
  92. package/src/components/dialogs/ExportSVGDialog.tsx +25 -1
  93. package/src/components/header/GappynessSlider.tsx +35 -0
  94. package/src/components/header/Header.tsx +3 -1
  95. package/src/components/header/HeaderMenu.tsx +36 -15
  96. package/src/components/minimap/MinimapSVG.tsx +6 -3
  97. package/src/components/msa/MSACanvasBlock.tsx +66 -48
  98. package/src/components/msa/renderMSABlock.ts +103 -40
  99. package/src/components/msa/renderMSAMouseover.ts +9 -0
  100. package/src/components/tracks/renderTracksSvg.ts +157 -0
  101. package/src/components/tree/TreeNodeMenu.tsx +2 -2
  102. package/src/components/tree/renderTreeCanvas.ts +25 -34
  103. package/src/constants.ts +27 -0
  104. package/src/layout.ts +1 -6
  105. package/src/model/msaModel.ts +4 -2
  106. package/src/model/treeModel.ts +19 -8
  107. package/src/model.ts +517 -140
  108. package/src/neighborJoining.test.ts +129 -0
  109. package/src/neighborJoining.ts +885 -0
  110. package/src/parsers/A3mMSA.test.ts +164 -0
  111. package/src/parsers/A3mMSA.ts +321 -0
  112. package/src/parsers/ClustalMSA.ts +7 -5
  113. package/src/parsers/FastaMSA.ts +17 -17
  114. package/src/renderToSvg.tsx +105 -26
  115. package/src/rowCoordinateCalculations.ts +2 -0
  116. package/src/types.ts +2 -4
  117. package/src/util.ts +21 -8
  118. package/src/version.ts +1 -1
  119. package/dist/components/dialogs/TracklistDialog.d.ts +0 -7
  120. package/dist/components/dialogs/TracklistDialog.js +0 -23
  121. package/dist/components/dialogs/TracklistDialog.js.map +0 -1
  122. package/src/components/dialogs/TracklistDialog.tsx +0 -73
@@ -0,0 +1,885 @@
1
+ // Neighbor Joining tree construction using BLOSUM62 distances
2
+ // Based on Saitou & Nei (1987) "The neighbor-joining method"
3
+
4
+ const BLOSUM62: Record<string, Record<string, number>> = {
5
+ A: {
6
+ A: 4,
7
+ R: -1,
8
+ N: -2,
9
+ D: -2,
10
+ C: 0,
11
+ Q: -1,
12
+ E: -1,
13
+ G: 0,
14
+ H: -2,
15
+ I: -1,
16
+ L: -1,
17
+ K: -1,
18
+ M: -1,
19
+ F: -2,
20
+ P: -1,
21
+ S: 1,
22
+ T: 0,
23
+ W: -3,
24
+ Y: -2,
25
+ V: 0,
26
+ B: -2,
27
+ Z: -1,
28
+ X: 0,
29
+ '*': -4,
30
+ },
31
+ R: {
32
+ A: -1,
33
+ R: 5,
34
+ N: 0,
35
+ D: -2,
36
+ C: -3,
37
+ Q: 1,
38
+ E: 0,
39
+ G: -2,
40
+ H: 0,
41
+ I: -3,
42
+ L: -2,
43
+ K: 2,
44
+ M: -1,
45
+ F: -3,
46
+ P: -2,
47
+ S: -1,
48
+ T: -1,
49
+ W: -3,
50
+ Y: -2,
51
+ V: -3,
52
+ B: -1,
53
+ Z: 0,
54
+ X: -1,
55
+ '*': -4,
56
+ },
57
+ N: {
58
+ A: -2,
59
+ R: 0,
60
+ N: 6,
61
+ D: 1,
62
+ C: -3,
63
+ Q: 0,
64
+ E: 0,
65
+ G: 0,
66
+ H: 1,
67
+ I: -3,
68
+ L: -3,
69
+ K: 0,
70
+ M: -2,
71
+ F: -3,
72
+ P: -2,
73
+ S: 1,
74
+ T: 0,
75
+ W: -4,
76
+ Y: -2,
77
+ V: -3,
78
+ B: 3,
79
+ Z: 0,
80
+ X: -1,
81
+ '*': -4,
82
+ },
83
+ D: {
84
+ A: -2,
85
+ R: -2,
86
+ N: 1,
87
+ D: 6,
88
+ C: -3,
89
+ Q: 0,
90
+ E: 2,
91
+ G: -1,
92
+ H: -1,
93
+ I: -3,
94
+ L: -4,
95
+ K: -1,
96
+ M: -3,
97
+ F: -3,
98
+ P: -1,
99
+ S: 0,
100
+ T: -1,
101
+ W: -4,
102
+ Y: -3,
103
+ V: -3,
104
+ B: 4,
105
+ Z: 1,
106
+ X: -1,
107
+ '*': -4,
108
+ },
109
+ C: {
110
+ A: 0,
111
+ R: -3,
112
+ N: -3,
113
+ D: -3,
114
+ C: 9,
115
+ Q: -3,
116
+ E: -4,
117
+ G: -3,
118
+ H: -3,
119
+ I: -1,
120
+ L: -1,
121
+ K: -3,
122
+ M: -1,
123
+ F: -2,
124
+ P: -3,
125
+ S: -1,
126
+ T: -1,
127
+ W: -2,
128
+ Y: -2,
129
+ V: -1,
130
+ B: -3,
131
+ Z: -3,
132
+ X: -2,
133
+ '*': -4,
134
+ },
135
+ Q: {
136
+ A: -1,
137
+ R: 1,
138
+ N: 0,
139
+ D: 0,
140
+ C: -3,
141
+ Q: 5,
142
+ E: 2,
143
+ G: -2,
144
+ H: 0,
145
+ I: -3,
146
+ L: -2,
147
+ K: 1,
148
+ M: 0,
149
+ F: -3,
150
+ P: -1,
151
+ S: 0,
152
+ T: -1,
153
+ W: -2,
154
+ Y: -1,
155
+ V: -2,
156
+ B: 0,
157
+ Z: 3,
158
+ X: -1,
159
+ '*': -4,
160
+ },
161
+ E: {
162
+ A: -1,
163
+ R: 0,
164
+ N: 0,
165
+ D: 2,
166
+ C: -4,
167
+ Q: 2,
168
+ E: 5,
169
+ G: -2,
170
+ H: 0,
171
+ I: -3,
172
+ L: -3,
173
+ K: 1,
174
+ M: -2,
175
+ F: -3,
176
+ P: -1,
177
+ S: 0,
178
+ T: -1,
179
+ W: -3,
180
+ Y: -2,
181
+ V: -2,
182
+ B: 1,
183
+ Z: 4,
184
+ X: -1,
185
+ '*': -4,
186
+ },
187
+ G: {
188
+ A: 0,
189
+ R: -2,
190
+ N: 0,
191
+ D: -1,
192
+ C: -3,
193
+ Q: -2,
194
+ E: -2,
195
+ G: 6,
196
+ H: -2,
197
+ I: -4,
198
+ L: -4,
199
+ K: -2,
200
+ M: -3,
201
+ F: -3,
202
+ P: -2,
203
+ S: 0,
204
+ T: -2,
205
+ W: -2,
206
+ Y: -3,
207
+ V: -3,
208
+ B: -1,
209
+ Z: -2,
210
+ X: -1,
211
+ '*': -4,
212
+ },
213
+ H: {
214
+ A: -2,
215
+ R: 0,
216
+ N: 1,
217
+ D: -1,
218
+ C: -3,
219
+ Q: 0,
220
+ E: 0,
221
+ G: -2,
222
+ H: 8,
223
+ I: -3,
224
+ L: -3,
225
+ K: -1,
226
+ M: -2,
227
+ F: -1,
228
+ P: -2,
229
+ S: -1,
230
+ T: -2,
231
+ W: -2,
232
+ Y: 2,
233
+ V: -3,
234
+ B: 0,
235
+ Z: 0,
236
+ X: -1,
237
+ '*': -4,
238
+ },
239
+ I: {
240
+ A: -1,
241
+ R: -3,
242
+ N: -3,
243
+ D: -3,
244
+ C: -1,
245
+ Q: -3,
246
+ E: -3,
247
+ G: -4,
248
+ H: -3,
249
+ I: 4,
250
+ L: 2,
251
+ K: -3,
252
+ M: 1,
253
+ F: 0,
254
+ P: -3,
255
+ S: -2,
256
+ T: -1,
257
+ W: -3,
258
+ Y: -1,
259
+ V: 3,
260
+ B: -3,
261
+ Z: -3,
262
+ X: -1,
263
+ '*': -4,
264
+ },
265
+ L: {
266
+ A: -1,
267
+ R: -2,
268
+ N: -3,
269
+ D: -4,
270
+ C: -1,
271
+ Q: -2,
272
+ E: -3,
273
+ G: -4,
274
+ H: -3,
275
+ I: 2,
276
+ L: 4,
277
+ K: -2,
278
+ M: 2,
279
+ F: 0,
280
+ P: -3,
281
+ S: -2,
282
+ T: -1,
283
+ W: -2,
284
+ Y: -1,
285
+ V: 1,
286
+ B: -4,
287
+ Z: -3,
288
+ X: -1,
289
+ '*': -4,
290
+ },
291
+ K: {
292
+ A: -1,
293
+ R: 2,
294
+ N: 0,
295
+ D: -1,
296
+ C: -3,
297
+ Q: 1,
298
+ E: 1,
299
+ G: -2,
300
+ H: -1,
301
+ I: -3,
302
+ L: -2,
303
+ K: 5,
304
+ M: -1,
305
+ F: -3,
306
+ P: -1,
307
+ S: 0,
308
+ T: -1,
309
+ W: -3,
310
+ Y: -2,
311
+ V: -2,
312
+ B: 0,
313
+ Z: 1,
314
+ X: -1,
315
+ '*': -4,
316
+ },
317
+ M: {
318
+ A: -1,
319
+ R: -1,
320
+ N: -2,
321
+ D: -3,
322
+ C: -1,
323
+ Q: 0,
324
+ E: -2,
325
+ G: -3,
326
+ H: -2,
327
+ I: 1,
328
+ L: 2,
329
+ K: -1,
330
+ M: 5,
331
+ F: 0,
332
+ P: -2,
333
+ S: -1,
334
+ T: -1,
335
+ W: -1,
336
+ Y: -1,
337
+ V: 1,
338
+ B: -3,
339
+ Z: -1,
340
+ X: -1,
341
+ '*': -4,
342
+ },
343
+ F: {
344
+ A: -2,
345
+ R: -3,
346
+ N: -3,
347
+ D: -3,
348
+ C: -2,
349
+ Q: -3,
350
+ E: -3,
351
+ G: -3,
352
+ H: -1,
353
+ I: 0,
354
+ L: 0,
355
+ K: -3,
356
+ M: 0,
357
+ F: 6,
358
+ P: -4,
359
+ S: -2,
360
+ T: -2,
361
+ W: 1,
362
+ Y: 3,
363
+ V: -1,
364
+ B: -3,
365
+ Z: -3,
366
+ X: -1,
367
+ '*': -4,
368
+ },
369
+ P: {
370
+ A: -1,
371
+ R: -2,
372
+ N: -2,
373
+ D: -1,
374
+ C: -3,
375
+ Q: -1,
376
+ E: -1,
377
+ G: -2,
378
+ H: -2,
379
+ I: -3,
380
+ L: -3,
381
+ K: -1,
382
+ M: -2,
383
+ F: -4,
384
+ P: 7,
385
+ S: -1,
386
+ T: -1,
387
+ W: -4,
388
+ Y: -3,
389
+ V: -2,
390
+ B: -2,
391
+ Z: -1,
392
+ X: -2,
393
+ '*': -4,
394
+ },
395
+ S: {
396
+ A: 1,
397
+ R: -1,
398
+ N: 1,
399
+ D: 0,
400
+ C: -1,
401
+ Q: 0,
402
+ E: 0,
403
+ G: 0,
404
+ H: -1,
405
+ I: -2,
406
+ L: -2,
407
+ K: 0,
408
+ M: -1,
409
+ F: -2,
410
+ P: -1,
411
+ S: 4,
412
+ T: 1,
413
+ W: -3,
414
+ Y: -2,
415
+ V: -2,
416
+ B: 0,
417
+ Z: 0,
418
+ X: 0,
419
+ '*': -4,
420
+ },
421
+ T: {
422
+ A: 0,
423
+ R: -1,
424
+ N: 0,
425
+ D: -1,
426
+ C: -1,
427
+ Q: -1,
428
+ E: -1,
429
+ G: -2,
430
+ H: -2,
431
+ I: -1,
432
+ L: -1,
433
+ K: -1,
434
+ M: -1,
435
+ F: -2,
436
+ P: -1,
437
+ S: 1,
438
+ T: 5,
439
+ W: -2,
440
+ Y: -2,
441
+ V: 0,
442
+ B: -1,
443
+ Z: -1,
444
+ X: 0,
445
+ '*': -4,
446
+ },
447
+ W: {
448
+ A: -3,
449
+ R: -3,
450
+ N: -4,
451
+ D: -4,
452
+ C: -2,
453
+ Q: -2,
454
+ E: -3,
455
+ G: -2,
456
+ H: -2,
457
+ I: -3,
458
+ L: -2,
459
+ K: -3,
460
+ M: -1,
461
+ F: 1,
462
+ P: -4,
463
+ S: -3,
464
+ T: -2,
465
+ W: 11,
466
+ Y: 2,
467
+ V: -3,
468
+ B: -4,
469
+ Z: -3,
470
+ X: -2,
471
+ '*': -4,
472
+ },
473
+ Y: {
474
+ A: -2,
475
+ R: -2,
476
+ N: -2,
477
+ D: -3,
478
+ C: -2,
479
+ Q: -1,
480
+ E: -2,
481
+ G: -3,
482
+ H: 2,
483
+ I: -1,
484
+ L: -1,
485
+ K: -2,
486
+ M: -1,
487
+ F: 3,
488
+ P: -3,
489
+ S: -2,
490
+ T: -2,
491
+ W: 2,
492
+ Y: 7,
493
+ V: -1,
494
+ B: -3,
495
+ Z: -2,
496
+ X: -1,
497
+ '*': -4,
498
+ },
499
+ V: {
500
+ A: 0,
501
+ R: -3,
502
+ N: -3,
503
+ D: -3,
504
+ C: -1,
505
+ Q: -2,
506
+ E: -2,
507
+ G: -3,
508
+ H: -3,
509
+ I: 3,
510
+ L: 1,
511
+ K: -2,
512
+ M: 1,
513
+ F: -1,
514
+ P: -2,
515
+ S: -2,
516
+ T: 0,
517
+ W: -3,
518
+ Y: -1,
519
+ V: 4,
520
+ B: -3,
521
+ Z: -2,
522
+ X: -1,
523
+ '*': -4,
524
+ },
525
+ B: {
526
+ A: -2,
527
+ R: -1,
528
+ N: 3,
529
+ D: 4,
530
+ C: -3,
531
+ Q: 0,
532
+ E: 1,
533
+ G: -1,
534
+ H: 0,
535
+ I: -3,
536
+ L: -4,
537
+ K: 0,
538
+ M: -3,
539
+ F: -3,
540
+ P: -2,
541
+ S: 0,
542
+ T: -1,
543
+ W: -4,
544
+ Y: -3,
545
+ V: -3,
546
+ B: 4,
547
+ Z: 1,
548
+ X: -1,
549
+ '*': -4,
550
+ },
551
+ Z: {
552
+ A: -1,
553
+ R: 0,
554
+ N: 0,
555
+ D: 1,
556
+ C: -3,
557
+ Q: 3,
558
+ E: 4,
559
+ G: -2,
560
+ H: 0,
561
+ I: -3,
562
+ L: -3,
563
+ K: 1,
564
+ M: -1,
565
+ F: -3,
566
+ P: -1,
567
+ S: 0,
568
+ T: -1,
569
+ W: -3,
570
+ Y: -2,
571
+ V: -2,
572
+ B: 1,
573
+ Z: 4,
574
+ X: -1,
575
+ '*': -4,
576
+ },
577
+ X: {
578
+ A: 0,
579
+ R: -1,
580
+ N: -1,
581
+ D: -1,
582
+ C: -2,
583
+ Q: -1,
584
+ E: -1,
585
+ G: -1,
586
+ H: -1,
587
+ I: -1,
588
+ L: -1,
589
+ K: -1,
590
+ M: -1,
591
+ F: -1,
592
+ P: -2,
593
+ S: 0,
594
+ T: 0,
595
+ W: -2,
596
+ Y: -1,
597
+ V: -1,
598
+ B: -1,
599
+ Z: -1,
600
+ X: -1,
601
+ '*': -4,
602
+ },
603
+ '*': {
604
+ A: -4,
605
+ R: -4,
606
+ N: -4,
607
+ D: -4,
608
+ C: -4,
609
+ Q: -4,
610
+ E: -4,
611
+ G: -4,
612
+ H: -4,
613
+ I: -4,
614
+ L: -4,
615
+ K: -4,
616
+ M: -4,
617
+ F: -4,
618
+ P: -4,
619
+ S: -4,
620
+ T: -4,
621
+ W: -4,
622
+ Y: -4,
623
+ V: -4,
624
+ B: -4,
625
+ Z: -4,
626
+ X: -4,
627
+ '*': 1,
628
+ },
629
+ }
630
+
631
+ function getBlosum62Score(a: string, b: string) {
632
+ const upper_a = a.toUpperCase()
633
+ const upper_b = b.toUpperCase()
634
+ return BLOSUM62[upper_a]?.[upper_b] ?? -4
635
+ }
636
+
637
+ function computePairwiseDistance(seq1: string, seq2: string) {
638
+ if (seq1.length !== seq2.length) {
639
+ throw new Error('Sequences must have the same length (aligned)')
640
+ }
641
+
642
+ let matches = 0
643
+ let mismatches = 0
644
+ let totalScore = 0
645
+ let maxPossibleScore = 0
646
+
647
+ for (let i = 0; i < seq1.length; i++) {
648
+ const a = seq1[i]!
649
+ const b = seq2[i]!
650
+
651
+ if (a === '-' && b === '-') {
652
+ continue
653
+ }
654
+
655
+ if (a === '-' || b === '-') {
656
+ mismatches++
657
+ continue
658
+ }
659
+
660
+ const score = getBlosum62Score(a, b)
661
+ totalScore += score
662
+ maxPossibleScore += Math.max(getBlosum62Score(a, a), getBlosum62Score(b, b))
663
+
664
+ if (a.toUpperCase() === b.toUpperCase()) {
665
+ matches++
666
+ } else {
667
+ mismatches++
668
+ }
669
+ }
670
+
671
+ const total = matches + mismatches
672
+ if (total === 0) {
673
+ return 1
674
+ }
675
+
676
+ // Convert similarity to distance using normalized BLOSUM62 score
677
+ // Higher scores mean more similar, so we invert
678
+ if (maxPossibleScore <= 0) {
679
+ return 1
680
+ }
681
+
682
+ const normalizedScore = totalScore / maxPossibleScore
683
+ // Clamp to valid range and convert to distance
684
+ const clampedScore = Math.max(0.01, Math.min(1, normalizedScore))
685
+
686
+ // Use Kimura-like correction: d = -ln(similarity)
687
+ return -Math.log(clampedScore)
688
+ }
689
+
690
+ function computeDistanceMatrix(rows: readonly [string, string][]) {
691
+ const n = rows.length
692
+ const distances: number[][] = []
693
+
694
+ for (let i = 0; i < n; i++) {
695
+ distances[i] = []
696
+ for (let j = 0; j < n; j++) {
697
+ if (i === j) {
698
+ distances[i]![j] = 0
699
+ } else if (j < i) {
700
+ distances[i]![j] = distances[j]![i]!
701
+ } else {
702
+ distances[i]![j] = computePairwiseDistance(rows[i]![1], rows[j]![1])
703
+ }
704
+ }
705
+ }
706
+
707
+ return distances
708
+ }
709
+
710
+ interface NJNode {
711
+ name?: string
712
+ left?: NJNode
713
+ right?: NJNode
714
+ leftLength?: number
715
+ rightLength?: number
716
+ }
717
+
718
+ function neighborJoining(distances: number[][], names: string[]): NJNode {
719
+ const n = distances.length
720
+ if (n < 2) {
721
+ return { name: names[0] }
722
+ }
723
+ if (n === 2) {
724
+ const d = distances[0]![1]!
725
+ return {
726
+ left: { name: names[0] },
727
+ right: { name: names[1] },
728
+ leftLength: d / 2,
729
+ rightLength: d / 2,
730
+ }
731
+ }
732
+
733
+ // Work with copies that we'll modify
734
+ const D: number[][] = []
735
+ for (let i = 0; i < n; i++) {
736
+ D[i] = [...distances[i]!]
737
+ }
738
+ const nodes: (NJNode | undefined)[] = names.map(name => ({ name }))
739
+
740
+ let remaining = n
741
+
742
+ while (remaining > 2) {
743
+ // Find active indices
744
+ const active: number[] = []
745
+ for (let i = 0; i < nodes.length; i++) {
746
+ if (nodes[i] !== undefined) {
747
+ active.push(i)
748
+ }
749
+ }
750
+
751
+ // Compute r values (sum of distances for each node)
752
+ const r = new Map<number, number>()
753
+ for (const i of active) {
754
+ let sum = 0
755
+ for (const j of active) {
756
+ if (i !== j) {
757
+ sum += D[i]![j]!
758
+ }
759
+ }
760
+ r.set(i, sum)
761
+ }
762
+
763
+ // Find pair with minimum Q value
764
+ let minQ = Infinity
765
+ let minI = -1
766
+ let minJ = -1
767
+
768
+ for (let ai = 0; ai < active.length; ai++) {
769
+ for (let aj = ai + 1; aj < active.length; aj++) {
770
+ const i = active[ai]!
771
+ const j = active[aj]!
772
+ const q = (remaining - 2) * D[i]![j]! - r.get(i)! - r.get(j)!
773
+
774
+ if (q < minQ) {
775
+ minQ = q
776
+ minI = i
777
+ minJ = j
778
+ }
779
+ }
780
+ }
781
+
782
+ // Calculate branch lengths
783
+ const dij = D[minI]![minJ]!
784
+ const ri = r.get(minI)!
785
+ const rj = r.get(minJ)!
786
+
787
+ let limbI: number
788
+ let limbJ: number
789
+
790
+ if (remaining > 2) {
791
+ limbI = dij / 2 + (ri - rj) / (2 * (remaining - 2))
792
+ limbJ = dij - limbI
793
+ } else {
794
+ limbI = dij / 2
795
+ limbJ = dij / 2
796
+ }
797
+
798
+ // Ensure non-negative branch lengths
799
+ limbI = Math.max(0, limbI)
800
+ limbJ = Math.max(0, limbJ)
801
+
802
+ // Create new node
803
+ const newNode: NJNode = {
804
+ left: nodes[minI],
805
+ right: nodes[minJ],
806
+ leftLength: limbI,
807
+ rightLength: limbJ,
808
+ }
809
+
810
+ // Update distance matrix
811
+ const newIdx = minI
812
+ for (const k of active) {
813
+ if (k !== minI && k !== minJ) {
814
+ const newDist = (D[minI]![k]! + D[minJ]![k]! - dij) / 2
815
+ D[newIdx]![k] = Math.max(0, newDist)
816
+ D[k]![newIdx] = Math.max(0, newDist)
817
+ }
818
+ }
819
+
820
+ // Mark minJ as removed
821
+ nodes[minJ] = undefined
822
+ nodes[newIdx] = newNode
823
+
824
+ remaining--
825
+ }
826
+
827
+ // Connect final two nodes
828
+ const finalActive: number[] = []
829
+ for (let i = 0; i < nodes.length; i++) {
830
+ if (nodes[i] !== undefined) {
831
+ finalActive.push(i)
832
+ }
833
+ }
834
+
835
+ if (finalActive.length === 2) {
836
+ const i = finalActive[0]!
837
+ const j = finalActive[1]!
838
+ const d = D[i]![j]!
839
+ return {
840
+ left: nodes[i],
841
+ right: nodes[j],
842
+ leftLength: d / 2,
843
+ rightLength: d / 2,
844
+ }
845
+ }
846
+
847
+ return nodes[finalActive[0]!]!
848
+ }
849
+
850
+ function nodeToNewick(node: NJNode, branchLength?: number): string {
851
+ let result: string
852
+
853
+ if (node.name !== undefined && !node.left && !node.right) {
854
+ // Leaf node - escape special characters in name
855
+ const escapedName = node.name.replace(/[():,;[\]]/g, '_')
856
+ result = escapedName
857
+ } else {
858
+ // Internal node
859
+ const leftNewick = node.left ? nodeToNewick(node.left, node.leftLength) : ''
860
+ const rightNewick = node.right
861
+ ? nodeToNewick(node.right, node.rightLength)
862
+ : ''
863
+ result = `(${leftNewick},${rightNewick})`
864
+ }
865
+
866
+ if (branchLength !== undefined) {
867
+ result += `:${branchLength.toFixed(6)}`
868
+ }
869
+
870
+ return result
871
+ }
872
+
873
+ export function calculateNeighborJoiningTree(
874
+ rows: readonly [string, string][],
875
+ ) {
876
+ if (rows.length < 2) {
877
+ throw new Error('Need at least 2 sequences to build a tree')
878
+ }
879
+
880
+ const names = rows.map(r => r[0])
881
+ const distances = computeDistanceMatrix(rows)
882
+ const tree = neighborJoining(distances, names)
883
+
884
+ return nodeToNewick(tree) + ';'
885
+ }