jbrowse-plugin-mafviewer 1.4.3 → 1.4.6

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 (204) hide show
  1. package/README.md +1 -1
  2. package/dist/BigMafAdapter/BigMafAdapter.js +4 -5
  3. package/dist/BigMafAdapter/BigMafAdapter.js.map +1 -1
  4. package/dist/BigMafAdapter/configSchema.d.ts +2 -2
  5. package/dist/LinearMafDisplay/components/LinearMafDisplayComponent.js +39 -109
  6. package/dist/LinearMafDisplay/components/LinearMafDisplayComponent.js.map +1 -1
  7. package/dist/LinearMafDisplay/components/MAFTooltip.d.ts +0 -3
  8. package/dist/LinearMafDisplay/components/MAFTooltip.js.map +1 -1
  9. package/dist/LinearMafDisplay/components/MsaHighlightOverlay.d.ts +9 -0
  10. package/dist/LinearMafDisplay/components/MsaHighlightOverlay.js +34 -0
  11. package/dist/LinearMafDisplay/components/MsaHighlightOverlay.js.map +1 -0
  12. package/dist/LinearMafDisplay/components/Sidebar/ColorLegend.js +2 -2
  13. package/dist/LinearMafDisplay/components/Sidebar/ColorLegend.js.map +1 -1
  14. package/dist/LinearMafDisplay/components/Sidebar/RectBg.d.ts +1 -1
  15. package/dist/LinearMafDisplay/components/Sidebar/RectBg.js +2 -3
  16. package/dist/LinearMafDisplay/components/Sidebar/RectBg.js.map +1 -1
  17. package/dist/LinearMafDisplay/components/Sidebar/SvgWrapper.js +81 -11
  18. package/dist/LinearMafDisplay/components/Sidebar/SvgWrapper.js.map +1 -1
  19. package/dist/LinearMafDisplay/components/Sidebar/Tree.js +30 -9
  20. package/dist/LinearMafDisplay/components/Sidebar/Tree.js.map +1 -1
  21. package/dist/LinearMafDisplay/components/Sidebar/YScaleBars.d.ts +0 -1
  22. package/dist/LinearMafDisplay/components/Sidebar/YScaleBars.js.map +1 -1
  23. package/dist/LinearMafDisplay/components/useDragSelection.d.ts +25 -0
  24. package/dist/LinearMafDisplay/components/useDragSelection.js +103 -0
  25. package/dist/LinearMafDisplay/components/useDragSelection.js.map +1 -0
  26. package/dist/LinearMafDisplay/configSchema.d.ts +3 -30
  27. package/dist/LinearMafDisplay/renderSvg.js +1 -1
  28. package/dist/LinearMafDisplay/renderSvg.js.map +1 -1
  29. package/dist/LinearMafDisplay/stateModel.d.ts +1090 -102
  30. package/dist/LinearMafDisplay/stateModel.js +156 -17
  31. package/dist/LinearMafDisplay/stateModel.js.map +1 -1
  32. package/dist/LinearMafDisplay/types.d.ts +2 -2
  33. package/dist/LinearMafDisplay/util.d.ts +6 -0
  34. package/dist/LinearMafDisplay/util.js +28 -6
  35. package/dist/LinearMafDisplay/util.js.map +1 -1
  36. package/dist/LinearMafRenderer/LinearMafRenderer.d.ts +43 -10
  37. package/dist/LinearMafRenderer/LinearMafRenderer.js +1 -1
  38. package/dist/LinearMafRenderer/LinearMafRenderer.js.map +1 -1
  39. package/dist/LinearMafRenderer/components/LinearMafRendering.d.ts +14 -5
  40. package/dist/LinearMafRenderer/components/LinearMafRendering.js +40 -20
  41. package/dist/LinearMafRenderer/components/LinearMafRendering.js.map +1 -1
  42. package/dist/LinearMafRenderer/configSchema.d.ts +1 -6
  43. package/dist/LinearMafRenderer/configSchema.js +1 -6
  44. package/dist/LinearMafRenderer/configSchema.js.map +1 -1
  45. package/dist/LinearMafRenderer/makeImageData.js +6 -7
  46. package/dist/LinearMafRenderer/makeImageData.js.map +1 -1
  47. package/dist/LinearMafRenderer/rendering/features.d.ts +0 -1
  48. package/dist/LinearMafRenderer/rendering/features.js +1 -14
  49. package/dist/LinearMafRenderer/rendering/features.js.map +1 -1
  50. package/dist/LinearMafRenderer/rendering/insertions.d.ts +1 -1
  51. package/dist/LinearMafRenderer/rendering/insertions.js +10 -8
  52. package/dist/LinearMafRenderer/rendering/insertions.js.map +1 -1
  53. package/dist/LinearMafRenderer/rendering/matches.d.ts +1 -1
  54. package/dist/LinearMafRenderer/rendering/matches.js +3 -15
  55. package/dist/LinearMafRenderer/rendering/matches.js.map +1 -1
  56. package/dist/LinearMafRenderer/rendering/mismatches.d.ts +1 -1
  57. package/dist/LinearMafRenderer/rendering/mismatches.js +3 -3
  58. package/dist/LinearMafRenderer/rendering/spatialIndex.js +8 -2
  59. package/dist/LinearMafRenderer/rendering/spatialIndex.js.map +1 -1
  60. package/dist/LinearMafRenderer/rendering/text.js +1 -3
  61. package/dist/LinearMafRenderer/rendering/text.js.map +1 -1
  62. package/dist/LinearMafRenderer/rendering/types.d.ts +6 -5
  63. package/dist/MafAddTrackWorkflow/AddTrackWorkflow.js +1 -1
  64. package/dist/MafAddTrackWorkflow/AddTrackWorkflow.js.map +1 -1
  65. package/dist/MafAddTrackWorkflow/index.js +1 -1
  66. package/dist/MafAddTrackWorkflow/index.js.map +1 -1
  67. package/dist/MafGetSequences/MafGetSequences.d.ts +1 -0
  68. package/dist/MafGetSequences/MafGetSequences.js +2 -1
  69. package/dist/MafGetSequences/MafGetSequences.js.map +1 -1
  70. package/dist/MafSequenceWidget/LabelsCanvas.d.ts +8 -0
  71. package/dist/MafSequenceWidget/LabelsCanvas.js +37 -0
  72. package/dist/MafSequenceWidget/LabelsCanvas.js.map +1 -0
  73. package/dist/MafSequenceWidget/MafSequenceHoverHighlight.d.ts +6 -0
  74. package/dist/MafSequenceWidget/MafSequenceHoverHighlight.js +52 -0
  75. package/dist/MafSequenceWidget/MafSequenceHoverHighlight.js.map +1 -0
  76. package/dist/MafSequenceWidget/MafSequenceHoverHighlightExtension.d.ts +2 -0
  77. package/dist/MafSequenceWidget/MafSequenceHoverHighlightExtension.js +12 -0
  78. package/dist/MafSequenceWidget/MafSequenceHoverHighlightExtension.js.map +1 -0
  79. package/dist/MafSequenceWidget/MafSequenceWidget.d.ts +6 -0
  80. package/dist/MafSequenceWidget/MafSequenceWidget.js +189 -0
  81. package/dist/MafSequenceWidget/MafSequenceWidget.js.map +1 -0
  82. package/dist/MafSequenceWidget/SequenceCanvas.d.ts +12 -0
  83. package/dist/MafSequenceWidget/SequenceCanvas.js +86 -0
  84. package/dist/MafSequenceWidget/SequenceCanvas.js.map +1 -0
  85. package/dist/MafSequenceWidget/SequenceDisplay.d.ts +12 -0
  86. package/dist/MafSequenceWidget/SequenceDisplay.js +117 -0
  87. package/dist/MafSequenceWidget/SequenceDisplay.js.map +1 -0
  88. package/dist/MafSequenceWidget/SequenceTooltip.d.ts +11 -0
  89. package/dist/MafSequenceWidget/SequenceTooltip.js +39 -0
  90. package/dist/MafSequenceWidget/SequenceTooltip.js.map +1 -0
  91. package/dist/MafSequenceWidget/baseColors.d.ts +3 -0
  92. package/dist/MafSequenceWidget/baseColors.js +64 -0
  93. package/dist/MafSequenceWidget/baseColors.js.map +1 -0
  94. package/dist/MafSequenceWidget/colToGenomePos.d.ts +13 -0
  95. package/dist/MafSequenceWidget/colToGenomePos.js +32 -0
  96. package/dist/MafSequenceWidget/colToGenomePos.js.map +1 -0
  97. package/dist/MafSequenceWidget/colToGenomePos.test.d.ts +1 -0
  98. package/dist/MafSequenceWidget/colToGenomePos.test.js +136 -0
  99. package/dist/MafSequenceWidget/colToGenomePos.test.js.map +1 -0
  100. package/dist/MafSequenceWidget/configSchema.d.ts +1 -0
  101. package/dist/MafSequenceWidget/configSchema.js +3 -0
  102. package/dist/MafSequenceWidget/configSchema.js.map +1 -0
  103. package/dist/MafSequenceWidget/constants.d.ts +4 -0
  104. package/dist/MafSequenceWidget/constants.js +5 -0
  105. package/dist/MafSequenceWidget/constants.js.map +1 -0
  106. package/dist/MafSequenceWidget/index.d.ts +2 -0
  107. package/dist/MafSequenceWidget/index.js +16 -0
  108. package/dist/MafSequenceWidget/index.js.map +1 -0
  109. package/dist/MafSequenceWidget/stateModelFactory.d.ts +67 -0
  110. package/dist/MafSequenceWidget/stateModelFactory.js +21 -0
  111. package/dist/MafSequenceWidget/stateModelFactory.js.map +1 -0
  112. package/dist/MafTabixAdapter/MafTabixAdapter.js +4 -35
  113. package/dist/MafTabixAdapter/MafTabixAdapter.js.map +1 -1
  114. package/dist/MafTabixAdapter/configSchema.d.ts +4 -4
  115. package/dist/MafTrack/configSchema.d.ts +16 -11
  116. package/dist/index.js +2 -0
  117. package/dist/index.js.map +1 -1
  118. package/dist/jbrowse-plugin-mafviewer.umd.production.min.js +12 -24
  119. package/dist/jbrowse-plugin-mafviewer.umd.production.min.js.map +4 -4
  120. package/dist/util/clipboard.d.ts +2 -0
  121. package/dist/util/clipboard.js +28 -0
  122. package/dist/util/clipboard.js.map +1 -0
  123. package/dist/util/fastaUtils.d.ts +2 -1
  124. package/dist/util/fastaUtils.js +93 -50
  125. package/dist/util/fastaUtils.js.map +1 -1
  126. package/dist/util/fastaUtils.test.js +190 -0
  127. package/dist/util/fastaUtils.test.js.map +1 -1
  128. package/dist/util/parseAssemblyName.d.ts +32 -0
  129. package/dist/util/parseAssemblyName.js +87 -0
  130. package/dist/util/parseAssemblyName.js.map +1 -0
  131. package/dist/util/parseAssemblyName.test.d.ts +1 -0
  132. package/dist/util/parseAssemblyName.test.js +269 -0
  133. package/dist/util/parseAssemblyName.test.js.map +1 -0
  134. package/package.json +12 -12
  135. package/src/BigMafAdapter/BigMafAdapter.ts +5 -5
  136. package/src/LinearMafDisplay/components/LinearMafDisplayComponent.tsx +63 -145
  137. package/src/LinearMafDisplay/components/MAFTooltip.tsx +0 -3
  138. package/src/LinearMafDisplay/components/MsaHighlightOverlay.tsx +62 -0
  139. package/src/LinearMafDisplay/components/Sidebar/ColorLegend.tsx +2 -6
  140. package/src/LinearMafDisplay/components/Sidebar/RectBg.tsx +8 -3
  141. package/src/LinearMafDisplay/components/Sidebar/SvgWrapper.tsx +117 -15
  142. package/src/LinearMafDisplay/components/Sidebar/Tree.tsx +53 -8
  143. package/src/LinearMafDisplay/components/Sidebar/YScaleBars.tsx +0 -1
  144. package/src/LinearMafDisplay/components/useDragSelection.ts +159 -0
  145. package/src/LinearMafDisplay/renderSvg.tsx +1 -1
  146. package/src/LinearMafDisplay/stateModel.ts +215 -20
  147. package/src/LinearMafDisplay/types.ts +2 -2
  148. package/src/LinearMafDisplay/util.ts +35 -7
  149. package/src/LinearMafRenderer/LinearMafRenderer.ts +3 -5
  150. package/src/LinearMafRenderer/components/LinearMafRendering.tsx +67 -33
  151. package/src/LinearMafRenderer/configSchema.ts +1 -6
  152. package/src/LinearMafRenderer/makeImageData.ts +5 -14
  153. package/src/LinearMafRenderer/rendering/features.ts +2 -36
  154. package/src/LinearMafRenderer/rendering/insertions.ts +13 -8
  155. package/src/LinearMafRenderer/rendering/matches.ts +2 -27
  156. package/src/LinearMafRenderer/rendering/mismatches.ts +3 -3
  157. package/src/LinearMafRenderer/rendering/spatialIndex.ts +9 -2
  158. package/src/LinearMafRenderer/rendering/text.ts +1 -2
  159. package/src/LinearMafRenderer/rendering/types.ts +8 -5
  160. package/src/MafAddTrackWorkflow/AddTrackWorkflow.tsx +1 -1
  161. package/src/MafAddTrackWorkflow/index.ts +1 -1
  162. package/src/MafGetSequences/MafGetSequences.ts +10 -2
  163. package/src/MafSequenceWidget/LabelsCanvas.tsx +58 -0
  164. package/src/MafSequenceWidget/MafSequenceHoverHighlight.tsx +83 -0
  165. package/src/MafSequenceWidget/MafSequenceHoverHighlightExtension.tsx +24 -0
  166. package/src/MafSequenceWidget/MafSequenceWidget.tsx +294 -0
  167. package/src/MafSequenceWidget/SequenceCanvas.tsx +136 -0
  168. package/src/MafSequenceWidget/SequenceDisplay.tsx +188 -0
  169. package/src/MafSequenceWidget/SequenceTooltip.tsx +70 -0
  170. package/src/MafSequenceWidget/baseColors.ts +76 -0
  171. package/src/MafSequenceWidget/colToGenomePos.test.ts +166 -0
  172. package/src/MafSequenceWidget/colToGenomePos.ts +40 -0
  173. package/src/MafSequenceWidget/configSchema.ts +3 -0
  174. package/src/MafSequenceWidget/constants.ts +4 -0
  175. package/src/MafSequenceWidget/index.ts +24 -0
  176. package/src/MafSequenceWidget/stateModelFactory.ts +43 -0
  177. package/src/MafTabixAdapter/MafTabixAdapter.ts +12 -51
  178. package/src/index.ts +2 -0
  179. package/src/util/__snapshots__/fastaUtils.test.ts.snap +35 -0
  180. package/src/util/clipboard.ts +35 -0
  181. package/src/util/fastaUtils.test.ts +199 -0
  182. package/src/util/fastaUtils.ts +118 -51
  183. package/src/util/parseAssemblyName.test.ts +350 -0
  184. package/src/util/parseAssemblyName.ts +106 -0
  185. package/dist/LinearMafDisplay/components/GetSequenceDialog/GetSequenceDialog.d.ts +0 -11
  186. package/dist/LinearMafDisplay/components/GetSequenceDialog/GetSequenceDialog.js +0 -97
  187. package/dist/LinearMafDisplay/components/GetSequenceDialog/GetSequenceDialog.js.map +0 -1
  188. package/dist/LinearMafDisplay/components/util.d.ts +0 -1
  189. package/dist/LinearMafDisplay/components/util.js +0 -8
  190. package/dist/LinearMafDisplay/components/util.js.map +0 -1
  191. package/dist/LinearMafRenderer/components/util.d.ts +0 -1
  192. package/dist/LinearMafRenderer/components/util.js +0 -13
  193. package/dist/LinearMafRenderer/components/util.js.map +0 -1
  194. package/dist/util/fetchSequences.d.ts +0 -18
  195. package/dist/util/fetchSequences.js +0 -39
  196. package/dist/util/fetchSequences.js.map +0 -1
  197. package/dist/util/useSequences.d.ts +0 -21
  198. package/dist/util/useSequences.js +0 -64
  199. package/dist/util/useSequences.js.map +0 -1
  200. package/src/LinearMafDisplay/components/GetSequenceDialog/GetSequenceDialog.tsx +0 -175
  201. package/src/LinearMafDisplay/components/util.ts +0 -7
  202. package/src/LinearMafRenderer/components/util.ts +0 -13
  203. package/src/util/fetchSequences.ts +0 -57
  204. package/src/util/useSequences.ts +0 -90
@@ -0,0 +1,76 @@
1
+ import type { Theme } from '@mui/material'
2
+
3
+ interface BasePalette {
4
+ A: { main: string; contrastText: string }
5
+ C: { main: string; contrastText: string }
6
+ G: { main: string; contrastText: string }
7
+ T: { main: string; contrastText: string }
8
+ }
9
+
10
+ function getBases(theme: Theme): BasePalette | undefined {
11
+ return (theme.palette as any).bases as BasePalette | undefined
12
+ }
13
+
14
+ export function getBaseColor(base: string, theme: Theme): string {
15
+ const bases = getBases(theme)
16
+ if (!bases) {
17
+ switch (base.toUpperCase()) {
18
+ case 'A':
19
+ return '#6dbf6d'
20
+ case 'C':
21
+ return '#6c6cff'
22
+ case 'G':
23
+ return '#ffb347'
24
+ case 'T':
25
+ case 'U':
26
+ return '#ff6b6b'
27
+ default:
28
+ return theme.palette.grey[500]
29
+ }
30
+ }
31
+
32
+ switch (base.toUpperCase()) {
33
+ case 'A':
34
+ return bases.A.main
35
+ case 'C':
36
+ return bases.C.main
37
+ case 'G':
38
+ return bases.G.main
39
+ case 'T':
40
+ case 'U':
41
+ return bases.T.main
42
+ default:
43
+ return theme.palette.grey[500]
44
+ }
45
+ }
46
+
47
+ export function getContrastText(base: string, theme: Theme): string {
48
+ const bases = getBases(theme)
49
+ if (!bases) {
50
+ switch (base.toUpperCase()) {
51
+ case 'A':
52
+ case 'C':
53
+ case 'T':
54
+ case 'U':
55
+ return '#fff'
56
+ case 'G':
57
+ return '#000'
58
+ default:
59
+ return theme.palette.common.white
60
+ }
61
+ }
62
+
63
+ switch (base.toUpperCase()) {
64
+ case 'A':
65
+ return bases.A.contrastText
66
+ case 'C':
67
+ return bases.C.contrastText
68
+ case 'G':
69
+ return bases.G.contrastText
70
+ case 'T':
71
+ case 'U':
72
+ return bases.T.contrastText
73
+ default:
74
+ return theme.palette.common.white
75
+ }
76
+ }
@@ -0,0 +1,166 @@
1
+ import { describe, expect, test } from 'vitest'
2
+
3
+ import { buildColToGenomePos, findRefSampleIndex } from './colToGenomePos'
4
+
5
+ describe('findRefSampleIndex', () => {
6
+ const samples = [
7
+ { id: 'mm10', label: 'Mouse' },
8
+ { id: 'hg38', label: 'Human' },
9
+ { id: 'panTro6', label: 'Chimp' },
10
+ ]
11
+
12
+ test('returns index of sample matching assemblyName', () => {
13
+ expect(findRefSampleIndex(samples, 'hg38')).toBe(1)
14
+ expect(findRefSampleIndex(samples, 'mm10')).toBe(0)
15
+ expect(findRefSampleIndex(samples, 'panTro6')).toBe(2)
16
+ })
17
+
18
+ test('returns 0 when no match found', () => {
19
+ expect(findRefSampleIndex(samples, 'galGal6')).toBe(0)
20
+ })
21
+
22
+ test('returns 0 when samples is undefined', () => {
23
+ expect(findRefSampleIndex(undefined, 'hg38')).toBe(0)
24
+ })
25
+
26
+ test('returns 0 when assemblyName is undefined', () => {
27
+ expect(findRefSampleIndex(samples, undefined)).toBe(0)
28
+ })
29
+
30
+ test('returns 0 when both are undefined', () => {
31
+ expect(findRefSampleIndex(undefined, undefined)).toBe(0)
32
+ })
33
+
34
+ test('returns 0 for empty samples array', () => {
35
+ expect(findRefSampleIndex([], 'hg38')).toBe(0)
36
+ })
37
+ })
38
+
39
+ describe('buildColToGenomePos', () => {
40
+ test('maps non-gap characters to sequential genomic positions', () => {
41
+ const result = buildColToGenomePos('ACGT', 100)
42
+ expect(result).toEqual([100, 101, 102, 103])
43
+ })
44
+
45
+ test('maps gaps to undefined', () => {
46
+ const result = buildColToGenomePos('AC-GT', 100)
47
+ expect(result).toEqual([100, 101, undefined, 102, 103])
48
+ })
49
+
50
+ test('handles multiple consecutive gaps', () => {
51
+ const result = buildColToGenomePos('A--G', 100)
52
+ expect(result).toEqual([100, undefined, undefined, 101])
53
+ })
54
+
55
+ test('handles gaps at start', () => {
56
+ const result = buildColToGenomePos('--ACG', 100)
57
+ expect(result).toEqual([undefined, undefined, 100, 101, 102])
58
+ })
59
+
60
+ test('handles gaps at end', () => {
61
+ const result = buildColToGenomePos('ACG--', 100)
62
+ expect(result).toEqual([100, 101, 102, undefined, undefined])
63
+ })
64
+
65
+ test('handles all gaps', () => {
66
+ const result = buildColToGenomePos('----', 100)
67
+ expect(result).toEqual([undefined, undefined, undefined, undefined])
68
+ })
69
+
70
+ test('handles empty sequence', () => {
71
+ const result = buildColToGenomePos('', 100)
72
+ expect(result).toEqual([])
73
+ })
74
+
75
+ test('works with different start positions', () => {
76
+ const result = buildColToGenomePos('AC-T', 50000)
77
+ expect(result).toEqual([50000, 50001, undefined, 50002])
78
+ })
79
+ })
80
+
81
+ describe('integration: hover highlight with gaps', () => {
82
+ const samples = [
83
+ { id: 'mm10', label: 'Mouse' },
84
+ { id: 'hg38', label: 'Human' },
85
+ { id: 'panTro6', label: 'Chimp' },
86
+ ]
87
+
88
+ test('gap in non-reference sample does not affect mapping', () => {
89
+ // Reference is hg38 (index 1)
90
+ // mm10 (index 0) has a gap, but hg38 does not
91
+ const sequences = [
92
+ 'A-GT', // mm10 - has gap at position 1
93
+ 'ACGT', // hg38 - no gap (this is the reference)
94
+ 'ACGT', // panTro6
95
+ ]
96
+
97
+ const refIdx = findRefSampleIndex(samples, 'hg38')
98
+ expect(refIdx).toBe(1)
99
+
100
+ const mapping = buildColToGenomePos(sequences[refIdx]!, 100)
101
+ // All positions should map to genomic coordinates since reference has no gaps
102
+ expect(mapping).toEqual([100, 101, 102, 103])
103
+ })
104
+
105
+ test('gap in reference sample maps to undefined', () => {
106
+ // Reference is hg38 (index 1)
107
+ // hg38 has a gap (insertion in other species)
108
+ const sequences = [
109
+ 'ACGT', // mm10
110
+ 'A-GT', // hg38 - has gap (this is the reference)
111
+ 'ATGT', // panTro6 - has T where hg38 has gap
112
+ ]
113
+
114
+ const refIdx = findRefSampleIndex(samples, 'hg38')
115
+ const mapping = buildColToGenomePos(sequences[refIdx]!, 100)
116
+ // Position 1 should be undefined because reference has gap there
117
+ expect(mapping).toEqual([100, undefined, 101, 102])
118
+ })
119
+
120
+ test('correctly handles reference as first sample', () => {
121
+ // Reference is mm10 (index 0)
122
+ const sequences = [
123
+ 'ACGT', // mm10 - reference
124
+ 'A-GT', // hg38 - has gap
125
+ 'ACGT', // panTro6
126
+ ]
127
+
128
+ const refIdx = findRefSampleIndex(samples, 'mm10')
129
+ expect(refIdx).toBe(0)
130
+
131
+ const mapping = buildColToGenomePos(sequences[refIdx]!, 100)
132
+ // All positions map because reference (mm10) has no gaps
133
+ expect(mapping).toEqual([100, 101, 102, 103])
134
+ })
135
+
136
+ test('correctly handles reference as last sample', () => {
137
+ // Reference is panTro6 (index 2)
138
+ const sequences = [
139
+ 'A-GT', // mm10 - has gap
140
+ 'A-GT', // hg38 - has gap
141
+ 'ACGT', // panTro6 - no gap (this is the reference)
142
+ ]
143
+
144
+ const refIdx = findRefSampleIndex(samples, 'panTro6')
145
+ expect(refIdx).toBe(2)
146
+
147
+ const mapping = buildColToGenomePos(sequences[refIdx]!, 100)
148
+ // All positions map because reference (panTro6) has no gaps
149
+ expect(mapping).toEqual([100, 101, 102, 103])
150
+ })
151
+
152
+ test('falls back to first sample when assembly not found', () => {
153
+ // Reference is galGal6 which doesn't exist in samples
154
+ const sequences = [
155
+ 'ACGT', // mm10
156
+ 'A-GT', // hg38
157
+ 'ACGT', // panTro6
158
+ ]
159
+
160
+ const refIdx = findRefSampleIndex(samples, 'galGal6')
161
+ expect(refIdx).toBe(0) // Falls back to first sample
162
+
163
+ const mapping = buildColToGenomePos(sequences[refIdx]!, 100)
164
+ expect(mapping).toEqual([100, 101, 102, 103])
165
+ })
166
+ })
@@ -0,0 +1,40 @@
1
+ import type { Sample } from '../LinearMafDisplay/types'
2
+
3
+ /**
4
+ * Find the index of the reference sample (matching the assembly name from the region)
5
+ * Falls back to 0 if no match is found
6
+ */
7
+ export function findRefSampleIndex(
8
+ samples: Sample[] | undefined,
9
+ assemblyName: string | undefined,
10
+ ): number {
11
+ if (!samples || !assemblyName) {
12
+ return 0
13
+ }
14
+ const idx = samples.findIndex(s => s.id === assemblyName)
15
+ return idx === -1 ? 0 : idx
16
+ }
17
+
18
+ /**
19
+ * Build a mapping from display column index to genomic position.
20
+ * Only the reference sequence determines genomic positions - gaps in the
21
+ * reference map to undefined (no genomic position), while gaps in other
22
+ * samples don't affect the mapping.
23
+ */
24
+ export function buildColToGenomePos(
25
+ refSequence: string,
26
+ regionStart: number,
27
+ ): (number | undefined)[] {
28
+ const mapping: (number | undefined)[] = []
29
+ let genomePos = regionStart
30
+
31
+ for (const char of refSequence) {
32
+ if (char === '-') {
33
+ mapping.push(undefined)
34
+ } else {
35
+ mapping.push(genomePos)
36
+ genomePos++
37
+ }
38
+ }
39
+ return mapping
40
+ }
@@ -0,0 +1,3 @@
1
+ import { ConfigurationSchema } from '@jbrowse/core/configuration'
2
+
3
+ export const configSchema = ConfigurationSchema('MafSequenceWidget', {})
@@ -0,0 +1,4 @@
1
+ export const CHAR_WIDTH = 12
2
+ export const ROW_HEIGHT = 16
3
+ export const FONT = 'bold 12px monospace'
4
+ export const LABEL_PADDING = 10
@@ -0,0 +1,24 @@
1
+ import { lazy } from 'react'
2
+
3
+ import WidgetType from '@jbrowse/core/pluggableElementTypes/WidgetType'
4
+
5
+ import MafSequenceHoverHighlightExtensionF from './MafSequenceHoverHighlightExtension'
6
+ import { configSchema } from './configSchema'
7
+ import { stateModelFactory } from './stateModelFactory'
8
+
9
+ import type PluginManager from '@jbrowse/core/PluginManager'
10
+
11
+ export default function MafSequenceWidgetF(pluginManager: PluginManager) {
12
+ pluginManager.addWidgetType(
13
+ () =>
14
+ new WidgetType({
15
+ name: 'MafSequenceWidget',
16
+ heading: 'MAF Sequence',
17
+ configSchema: configSchema,
18
+ stateModel: stateModelFactory(),
19
+ ReactComponent: lazy(() => import('./MafSequenceWidget')),
20
+ }),
21
+ )
22
+
23
+ MafSequenceHoverHighlightExtensionF(pluginManager)
24
+ }
@@ -0,0 +1,43 @@
1
+ import { types } from '@jbrowse/mobx-state-tree'
2
+
3
+ import type { Sample } from '../LinearMafDisplay/types'
4
+ import type { AnyConfigurationModel } from '@jbrowse/core/configuration'
5
+ import type { Instance } from '@jbrowse/mobx-state-tree'
6
+
7
+ export interface HoverHighlight {
8
+ refName: string
9
+ start: number
10
+ end: number
11
+ assemblyName: string
12
+ }
13
+
14
+ export function stateModelFactory() {
15
+ return types
16
+ .model('MafSequenceWidget', {
17
+ id: types.identifier,
18
+ type: types.literal('MafSequenceWidget'),
19
+ adapterConfig: types.frozen<AnyConfigurationModel | undefined>(undefined),
20
+ samples: types.frozen<Sample[] | undefined>(undefined),
21
+ regions: types.frozen<
22
+ | {
23
+ refName: string
24
+ start: number
25
+ end: number
26
+ assemblyName: string
27
+ }[]
28
+ | undefined
29
+ >(undefined),
30
+ connectedViewId: types.maybe(types.string),
31
+ })
32
+ .volatile(() => ({
33
+ hoverHighlight: undefined as HoverHighlight | undefined,
34
+ }))
35
+ .actions(self => ({
36
+ setHoverHighlight(highlight: HoverHighlight | undefined) {
37
+ self.hoverHighlight = highlight
38
+ },
39
+ }))
40
+ }
41
+
42
+ export type MafSequenceWidgetStateModel = ReturnType<typeof stateModelFactory>
43
+ export type MafSequenceWidgetModel = Instance<MafSequenceWidgetStateModel>
@@ -10,11 +10,15 @@ import {
10
10
  } from '@jbrowse/core/util'
11
11
  import { openLocation } from '@jbrowse/core/util/io'
12
12
  import { ObservableCreate } from '@jbrowse/core/util/rxjs'
13
- import { getSnapshot } from 'mobx-state-tree'
13
+ import { getSnapshot } from '@jbrowse/mobx-state-tree'
14
14
  import { firstValueFrom, toArray } from 'rxjs'
15
15
 
16
16
  import parseNewick from '../parseNewick'
17
17
  import { normalize } from '../util'
18
+ import {
19
+ parseAssemblyAndChr,
20
+ selectReferenceSequence,
21
+ } from '../util/parseAssemblyName'
18
22
 
19
23
  interface OrganismRecord {
20
24
  chr: string
@@ -107,53 +111,7 @@ export default class MafTabixAdapter extends BaseFeatureDataAdapter {
107
111
  continue
108
112
  }
109
113
 
110
- // Optimized assembly name parsing with simplified logic
111
- let assemblyName: string
112
- let chr: string
113
-
114
- const firstDotIndex = assemblyAndChr.indexOf('.')
115
- if (firstDotIndex === -1) {
116
- // No dot found, entire string is assembly name
117
- assemblyName = assemblyAndChr
118
- chr = ''
119
- } else {
120
- const secondDotIndex = assemblyAndChr.indexOf(
121
- '.',
122
- firstDotIndex + 1,
123
- )
124
- if (secondDotIndex === -1) {
125
- // Only one dot: assembly.chr
126
- assemblyName = assemblyAndChr.slice(
127
- 0,
128
- Math.max(0, firstDotIndex),
129
- )
130
- chr = assemblyAndChr.slice(Math.max(0, firstDotIndex + 1))
131
- } else {
132
- // Multiple dots: check if second part is numeric (version number)
133
- const secondPart = assemblyAndChr.slice(
134
- firstDotIndex + 1,
135
- secondDotIndex,
136
- )
137
- const isNumeric =
138
- secondPart.length > 0 && !Number.isNaN(+secondPart)
139
-
140
- if (isNumeric) {
141
- // assembly.version.chr format
142
- assemblyName = assemblyAndChr.slice(
143
- 0,
144
- Math.max(0, secondDotIndex),
145
- )
146
- chr = assemblyAndChr.slice(Math.max(0, secondDotIndex + 1))
147
- } else {
148
- // assembly.chr.more format
149
- assemblyName = assemblyAndChr.slice(
150
- 0,
151
- Math.max(0, firstDotIndex),
152
- )
153
- chr = assemblyAndChr.slice(Math.max(0, firstDotIndex + 1))
154
- }
155
- }
156
- }
114
+ const { assemblyName, chr } = parseAssemblyAndChr(assemblyAndChr)
157
115
 
158
116
  if (assemblyName) {
159
117
  // Set first assembly name found (only once)
@@ -183,9 +141,12 @@ export default class MafTabixAdapter extends BaseFeatureDataAdapter {
183
141
  name: feature.get('name'),
184
142
  score: feature.get('score'),
185
143
  alignments,
186
- seq:
187
- alignments[refAssemblyName || query.assemblyName]?.seq ||
188
- alignments[firstAssemblyNameFound]?.seq,
144
+ seq: selectReferenceSequence(
145
+ alignments,
146
+ refAssemblyName,
147
+ query.assemblyName,
148
+ firstAssemblyNameFound,
149
+ ),
189
150
  },
190
151
  }),
191
152
  )
package/src/index.ts CHANGED
@@ -8,6 +8,7 @@ import LinearMafRendererF from './LinearMafRenderer'
8
8
  import MafAddTrackWorkflowF from './MafAddTrackWorkflow'
9
9
  import MafGetSamplesF from './MafGetSamples'
10
10
  import MafGetSequencesF from './MafGetSequences'
11
+ import MafSequenceWidgetF from './MafSequenceWidget'
11
12
  import MafTabixAdapterF from './MafTabixAdapter'
12
13
  import MafTrackF from './MafTrack'
13
14
 
@@ -24,6 +25,7 @@ export default class MafViewerPlugin extends Plugin {
24
25
  MafAddTrackWorkflowF(pluginManager)
25
26
  MafGetSequencesF(pluginManager)
26
27
  MafGetSamplesF(pluginManager)
28
+ MafSequenceWidgetF(pluginManager)
27
29
  }
28
30
 
29
31
  configure(_pluginManager: PluginManager) {}
@@ -7,6 +7,41 @@ exports[`gap in assembly1 1`] = `
7
7
  ]
8
8
  `;
9
9
 
10
+ exports[`includeInsertions - insertions at multiple positions 1`] = `
11
+ [
12
+ "ATCGGTAC",
13
+ "A-CG-TAC",
14
+ ]
15
+ `;
16
+
17
+ exports[`includeInsertions - insertions in multiple samples with different lengths 1`] = `
18
+ [
19
+ "AC-T-GTAC",
20
+ "ACTTTGTAC",
21
+ ]
22
+ `;
23
+
24
+ exports[`includeInsertions - single insertion in one sample 1`] = `
25
+ [
26
+ "AC--GTAC",
27
+ "ACTTGTAC",
28
+ ]
29
+ `;
30
+
31
+ exports[`includeInsertions with no insertions present 1`] = `
32
+ [
33
+ "ACGTA",
34
+ "AC-TT",
35
+ ]
36
+ `;
37
+
38
+ exports[`includeInsertions=false ignores insertions 1`] = `
39
+ [
40
+ "ACGTAC",
41
+ "ACGTAC",
42
+ ]
43
+ `;
44
+
10
45
  exports[`no showAllLetters 1`] = `
11
46
  [
12
47
  ".....",
@@ -0,0 +1,35 @@
1
+ export async function copyToClipboard(
2
+ text: string,
3
+ onSuccess?: () => void,
4
+ onError?: (e: unknown) => void,
5
+ ) {
6
+ try {
7
+ await navigator.clipboard.writeText(text)
8
+ onSuccess?.()
9
+ } catch (e) {
10
+ console.error(e)
11
+ onError?.(e)
12
+ }
13
+ }
14
+
15
+ export function downloadAsFile(
16
+ content: string,
17
+ filename: string,
18
+ onSuccess?: () => void,
19
+ onError?: (e: unknown) => void,
20
+ ) {
21
+ try {
22
+ const url = URL.createObjectURL(new Blob([content], { type: 'text/plain' }))
23
+ const a = document.createElement('a')
24
+ a.href = url
25
+ a.download = filename
26
+ document.body.append(a)
27
+ a.click()
28
+ a.remove()
29
+ URL.revokeObjectURL(url)
30
+ onSuccess?.()
31
+ } catch (e) {
32
+ console.error(e)
33
+ onError?.(e)
34
+ }
35
+ }