jbrowse-plugin-graphgenomeviewer 7.2.0 → 7.3.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 (115) hide show
  1. package/dist/chunks/GbzBaseSyntenyAdapter-FXMGIM7Z.js +2 -0
  2. package/dist/chunks/{GbzBaseSyntenyAdapter-NB6WBBZH.js.map → GbzBaseSyntenyAdapter-FXMGIM7Z.js.map} +3 -3
  3. package/dist/chunks/{GraphGenomeView-4O5F5I37.js → GraphGenomeView-WJ3FIDY2.js} +2 -2
  4. package/dist/chunks/{GraphTrackSettingsDialog-WZEJAF7F.js → GraphTrackSettingsDialog-ZW7ALMDV.js} +2 -2
  5. package/dist/chunks/{HighlightColorDialog-SRT2FPXP.js → HighlightColorDialog-VBZRUKD5.js} +2 -2
  6. package/dist/chunks/{LinearGraphDisplay-Z33PVCJL.js → LinearGraphDisplay-SPOPTJEN.js} +2 -2
  7. package/dist/chunks/MinigraphBubbleAdapter-QCOMFPLL.js +2 -0
  8. package/dist/chunks/{MinigraphBubbleAdapter-HDLUZ6YF.js.map → MinigraphBubbleAdapter-QCOMFPLL.js.map} +1 -1
  9. package/dist/chunks/RgfaTabixAdapter-O3NMNRZ2.js +2 -0
  10. package/dist/chunks/RgfaTabixAdapter-O3NMNRZ2.js.map +7 -0
  11. package/dist/chunks/WalkTabixSyntenyAdapter-7VDCNSFD.js +2 -0
  12. package/dist/chunks/WalkTabixSyntenyAdapter-7VDCNSFD.js.map +7 -0
  13. package/dist/chunks/chunk-2M4XYSLG.js +2 -0
  14. package/dist/chunks/chunk-2M4XYSLG.js.map +7 -0
  15. package/dist/chunks/{chunk-JLGPQO5L.js → chunk-2O3YVA2I.js} +2 -2
  16. package/dist/chunks/{chunk-CWNJR5XB.js → chunk-44CJ2N3N.js} +1 -1
  17. package/dist/chunks/{chunk-CWNJR5XB.js.map → chunk-44CJ2N3N.js.map} +1 -1
  18. package/dist/chunks/chunk-6KUHGRHU.js +2 -0
  19. package/dist/chunks/chunk-6KUHGRHU.js.map +7 -0
  20. package/dist/chunks/chunk-6MUBJVBG.js +2 -0
  21. package/dist/chunks/chunk-6MUBJVBG.js.map +7 -0
  22. package/dist/chunks/{chunk-GUXZESDR.js → chunk-7HWHWPRW.js} +1 -1
  23. package/dist/chunks/{chunk-GUXZESDR.js.map → chunk-7HWHWPRW.js.map} +1 -1
  24. package/dist/chunks/chunk-B2EP6FOQ.js +3 -0
  25. package/dist/chunks/chunk-B2EP6FOQ.js.map +7 -0
  26. package/dist/chunks/{chunk-44HZPYGC.js → chunk-BXRTG7FU.js} +2 -2
  27. package/dist/chunks/{chunk-44HZPYGC.js.map → chunk-BXRTG7FU.js.map} +1 -1
  28. package/dist/chunks/{chunk-HLKZCTQ3.js → chunk-DBPEUVUC.js} +1 -1
  29. package/dist/chunks/{chunk-HLKZCTQ3.js.map → chunk-DBPEUVUC.js.map} +1 -1
  30. package/dist/chunks/{chunk-FME624X3.js → chunk-DUR437TH.js} +2 -2
  31. package/dist/chunks/{chunk-FME624X3.js.map → chunk-DUR437TH.js.map} +1 -1
  32. package/dist/chunks/{chunk-6WA6WF6A.js → chunk-FIRQAKMX.js} +1 -1
  33. package/dist/chunks/{chunk-6WA6WF6A.js.map → chunk-FIRQAKMX.js.map} +1 -1
  34. package/dist/chunks/chunk-JCAZTNYG.js +2 -0
  35. package/dist/chunks/chunk-JCAZTNYG.js.map +7 -0
  36. package/dist/chunks/chunk-N74HGPAW.js +3 -0
  37. package/dist/chunks/chunk-N74HGPAW.js.map +7 -0
  38. package/dist/chunks/chunk-PEDWEPNK.js +2 -0
  39. package/dist/chunks/chunk-PEDWEPNK.js.map +7 -0
  40. package/dist/chunks/chunk-PYQG3JHP.js +3 -0
  41. package/dist/chunks/chunk-PYQG3JHP.js.map +7 -0
  42. package/dist/chunks/chunk-RFDSEB2X.js +4 -0
  43. package/dist/chunks/chunk-RFDSEB2X.js.map +7 -0
  44. package/dist/chunks/{chunk-O7Y7LYN6.js → chunk-RW775LQN.js} +2 -2
  45. package/dist/chunks/{chunk-O7Y7LYN6.js.map → chunk-RW775LQN.js.map} +1 -1
  46. package/dist/chunks/{chunk-N4WBX2OS.js → chunk-VQ77X5QU.js} +2 -2
  47. package/dist/chunks/{chunk-N4WBX2OS.js.map → chunk-VQ77X5QU.js.map} +1 -1
  48. package/dist/chunks/{chunk-LXCMWL7B.js → chunk-WUSKVEA6.js} +2 -2
  49. package/dist/chunks/{chunk-LXCMWL7B.js.map → chunk-WUSKVEA6.js.map} +1 -1
  50. package/dist/chunks/{chunk-ZNT7TQLN.js → chunk-YKSUKPYY.js} +2 -2
  51. package/dist/chunks/chunk-YKSUKPYY.js.map +7 -0
  52. package/dist/chunks/{renderSvg-RRWCAEGF.js → renderSvg-WD6YDV2I.js} +2 -2
  53. package/dist/jbrowse-plugin-graphgenomeviewer.esm.js +4 -4
  54. package/dist/jbrowse-plugin-graphgenomeviewer.esm.js.map +4 -4
  55. package/package.json +3 -3
  56. package/src/GbzBaseSyntenyAdapter/GbzBaseSyntenyAdapter.test.ts +2 -2
  57. package/src/GbzBaseSyntenyAdapter/GbzBaseSyntenyAdapter.ts +1 -1
  58. package/src/GetSubgraph.ts +1 -1
  59. package/src/GraphGenomeView/genes/genes.test.ts +20 -2
  60. package/src/GraphGenomeView/legendSections.test.ts +1 -1
  61. package/src/GraphGenomeView/legendSections.ts +1 -7
  62. package/src/GraphGenomeView/model.test.ts +1 -2
  63. package/src/GraphGenomeView/pane/graphViews.ts +24 -35
  64. package/src/GraphGenomeView/pane/loadActions.ts +4 -3
  65. package/src/GraphGenomeView/pane/paneBase.ts +1 -1
  66. package/src/GraphGenomeView/subgraphLoad.test.ts +41 -1
  67. package/src/GraphGenomeView/walkRowsCut.ts +10 -44
  68. package/src/RgfaTabixAdapter/RgfaTabixAdapter.test.ts +23 -4
  69. package/src/RgfaTabixAdapter/RgfaTabixAdapter.ts +2 -3
  70. package/src/RgfaTabixAdapter/bubbleTier.test.ts +1 -1
  71. package/src/RgfaTabixAdapter/test_data/walks3_inversion.GRCh38.walks.bed.gz +0 -0
  72. package/src/RgfaTabixAdapter/test_data/walks3_inversion.GRCh38.walks.bed.gz.tbi +0 -0
  73. package/src/RgfaTabixAdapter/walkCut.ts +20 -0
  74. package/src/RgfaTabixAdapter/walkFiles.test.ts +2 -2
  75. package/src/RgfaTabixAdapter/walkReader.ts +1 -1
  76. package/src/WalkTabixSyntenyAdapter/WalkTabixSyntenyAdapter.test.ts +77 -1
  77. package/src/WalkTabixSyntenyAdapter/WalkTabixSyntenyAdapter.ts +2 -2
  78. package/src/WalkTabixSyntenyAdapter/walkLanes.ts +55 -18
  79. package/src/version.ts +1 -1
  80. package/dist/chunks/GbzBaseSyntenyAdapter-NB6WBBZH.js +0 -2
  81. package/dist/chunks/MinigraphBubbleAdapter-HDLUZ6YF.js +0 -2
  82. package/dist/chunks/RgfaTabixAdapter-XZRTWQMZ.js +0 -3
  83. package/dist/chunks/RgfaTabixAdapter-XZRTWQMZ.js.map +0 -7
  84. package/dist/chunks/WalkTabixSyntenyAdapter-WBHOZLAN.js +0 -2
  85. package/dist/chunks/WalkTabixSyntenyAdapter-WBHOZLAN.js.map +0 -7
  86. package/dist/chunks/chunk-BWP3XYKD.js +0 -2
  87. package/dist/chunks/chunk-BWP3XYKD.js.map +0 -7
  88. package/dist/chunks/chunk-EDSIYH4H.js +0 -2
  89. package/dist/chunks/chunk-EDSIYH4H.js.map +0 -7
  90. package/dist/chunks/chunk-III4GT4G.js +0 -2
  91. package/dist/chunks/chunk-III4GT4G.js.map +0 -7
  92. package/dist/chunks/chunk-N5GYTUIL.js +0 -3
  93. package/dist/chunks/chunk-N5GYTUIL.js.map +0 -7
  94. package/dist/chunks/chunk-PA4HKAUJ.js +0 -2
  95. package/dist/chunks/chunk-PA4HKAUJ.js.map +0 -7
  96. package/dist/chunks/chunk-PFAHNHMM.js +0 -2
  97. package/dist/chunks/chunk-PFAHNHMM.js.map +0 -7
  98. package/dist/chunks/chunk-UFQTIWSN.js +0 -4
  99. package/dist/chunks/chunk-UFQTIWSN.js.map +0 -7
  100. package/dist/chunks/chunk-VDHDEEMF.js +0 -2
  101. package/dist/chunks/chunk-VDHDEEMF.js.map +0 -7
  102. package/dist/chunks/chunk-WGVATZLM.js +0 -3
  103. package/dist/chunks/chunk-WGVATZLM.js.map +0 -7
  104. package/dist/chunks/chunk-ZNT7TQLN.js.map +0 -7
  105. package/src/RgfaTabixAdapter/rgfaBed.test.ts +0 -254
  106. package/src/RgfaTabixAdapter/rgfaBed.ts +0 -314
  107. package/src/RgfaTabixAdapter/rgfaCut.ts +0 -139
  108. package/src/RgfaTabixAdapter/walkRowRuns.test.ts +0 -192
  109. package/src/RgfaTabixAdapter/walkRowRuns.ts +0 -520
  110. /package/dist/chunks/{GraphGenomeView-4O5F5I37.js.map → GraphGenomeView-WJ3FIDY2.js.map} +0 -0
  111. /package/dist/chunks/{GraphTrackSettingsDialog-WZEJAF7F.js.map → GraphTrackSettingsDialog-ZW7ALMDV.js.map} +0 -0
  112. /package/dist/chunks/{HighlightColorDialog-SRT2FPXP.js.map → HighlightColorDialog-VBZRUKD5.js.map} +0 -0
  113. /package/dist/chunks/{LinearGraphDisplay-Z33PVCJL.js.map → LinearGraphDisplay-SPOPTJEN.js.map} +0 -0
  114. /package/dist/chunks/{chunk-JLGPQO5L.js.map → chunk-2O3YVA2I.js.map} +0 -0
  115. /package/dist/chunks/{renderSvg-RRWCAEGF.js.map → renderSvg-WD6YDV2I.js.map} +0 -0
@@ -1,7 +0,0 @@
1
- {
2
- "version": 3,
3
- "sources": ["../../src/RgfaTabixAdapter/walkReader.ts"],
4
- "sourcesContent": ["import { resolvePanSNPrefix } from '@jbrowse/bandage-core/pansn'\nimport { WalkReader, referenceNodes, walksKept } from '@jbrowse/graph-walks'\n\nimport { getLines } from '../tabixRange.ts'\nimport { walkCutFor } from './walkRowRuns.ts'\n\nimport type { SubgraphAdapterOptions } from '../GetSubgraph.ts'\nimport type { TabixIndexedFile } from '@gmod/tabix'\nimport type {\n BaseFeatureDataAdapter,\n BaseOptions,\n} from '@jbrowse/core/data_adapters/BaseAdapter'\n\n// The zoom-in notices name the config slots that set the budgets\nexport const BUDGET_SLOTS = {\n steps: 'walkStepBudget',\n bytes: 'walkByteBudget',\n}\n\n/**\n * A walk file and the answers both walk-indexed adapters give from it,\n * RgfaTabixAdapter and WalkTabixSyntenyAdapter, whose schemas declare the\n * budget slots. @jbrowse/graph-walks reads and cuts the file.\n */\nexport class WalkIndex {\n readonly reader: WalkReader\n\n constructor(\n readonly file: TabixIndexedFile,\n private adapter: BaseFeatureDataAdapter,\n ) {\n this.reader = new WalkReader(file)\n }\n\n /**\n * The haplotypes the header names, `sample#haplotype` in byte order, or\n * undefined when it names none\n */\n async haplotypeNames(opts: BaseOptions = {}) {\n const { haplotypes } = await this.reader.header(opts)\n return haplotypes.length > 0 ? haplotypes : undefined\n }\n\n budgets() {\n return {\n stepBudget: this.adapter.getConf('walkStepBudget') as number,\n byteBudget: this.adapter.getConf('walkByteBudget') as number,\n budgetNames: BUDGET_SLOTS,\n }\n }\n\n /**\n * The graph display's cut of a window: the reference walk, the walks of\n * `haplotypes` (every walk when unset), and the nodes and links they visit.\n * Walk rows draw runs, so the step budget spares them.\n */\n async subgraph(\n refName: string,\n region: { start: number; end: number },\n { signal, haplotypes, walkRows }: SubgraphAdapterOptions,\n ) {\n const budgets = this.budgets()\n const { graph, kept, fragments } = await this.reader.cut(refName, region, {\n ...budgets,\n stepBudget: walkRows ? Infinity : budgets.stepBudget,\n signal,\n keep: walksKept(\n haplotypes?.map(lane => resolvePanSNPrefix(this.adapter, lane)),\n refName,\n ),\n })\n return walkCutFor(graph.tables(kept, fragments), region, walkRows)\n }\n\n /**\n * The reference nodes overlapping a window, from the ref rows of its\n * chunks. A row's tabix interval is its chunk's first base, and gfa-to-tabix\n * files a node crossing in from a chunk before under that chunk, so the read\n * starts at queryStart.\n */\n async referenceNodes(\n refName: string,\n region: { start: number; end: number },\n opts: BaseOptions,\n ) {\n const refRows: string[][] = []\n await getLines(\n this.file,\n refName,\n await this.reader.queryStart(region.start, opts),\n region.end,\n {\n signal: opts.signal,\n lineCallback: line => {\n const cols = line.split('\\t')\n if (cols[3] === 'ref') {\n refRows.push(cols)\n }\n },\n },\n )\n const seen = new Set<number>()\n return referenceNodes(refRows).filter(node => {\n if (\n node.refName !== refName ||\n node.end <= region.start ||\n node.start >= region.end ||\n seen.has(node.id)\n ) {\n return false\n }\n seen.add(node.id)\n return true\n })\n }\n}\n"],
5
- "mappings": "6IAcO,IAAMA,EAAe,CAC1B,MAAO,iBACP,MAAO,gBACT,EAOaC,EAAN,KAAgB,CAGrB,YACWC,EACDC,EACR,CAFS,UAAAD,EACD,aAAAC,EAER,KAAK,OAAS,IAAIC,EAAWF,CAAI,CACnC,CAMA,MAAM,eAAeG,EAAoB,CAAC,EAAG,CAC3C,GAAM,CAAE,WAAAC,CAAW,EAAI,MAAM,KAAK,OAAO,OAAOD,CAAI,EACpD,OAAOC,EAAW,OAAS,EAAIA,EAAa,MAC9C,CAEA,SAAU,CACR,MAAO,CACL,WAAY,KAAK,QAAQ,QAAQ,gBAAgB,EACjD,WAAY,KAAK,QAAQ,QAAQ,gBAAgB,EACjD,YAAaN,CACf,CACF,CAOA,MAAM,SACJO,EACAC,EACA,CAAE,OAAAC,EAAQ,WAAAH,EAAY,SAAAI,CAAS,EAC/B,CACA,IAAMC,EAAU,KAAK,QAAQ,EACvB,CAAE,MAAAC,EAAO,KAAAC,EAAM,UAAAC,CAAU,EAAI,MAAM,KAAK,OAAO,IAAIP,EAASC,EAAQ,CACxE,GAAGG,EACH,WAAYD,EAAW,IAAWC,EAAQ,WAC1C,OAAAF,EACA,KAAMM,EACJT,GAAY,IAAIU,GAAQC,EAAmB,KAAK,QAASD,CAAI,CAAC,EAC9DT,CACF,CACF,CAAC,EACD,OAAOW,EAAWN,EAAM,OAAOC,EAAMC,CAAS,EAAGN,EAAQE,CAAQ,CACnE,CAQA,MAAM,eACJH,EACAC,EACAH,EACA,CACA,IAAMc,EAAsB,CAAC,EAC7B,MAAMC,EACJ,KAAK,KACLb,EACA,MAAM,KAAK,OAAO,WAAWC,EAAO,MAAOH,CAAI,EAC/CG,EAAO,IACP,CACE,OAAQH,EAAK,OACb,aAAcgB,GAAQ,CACpB,IAAMC,EAAOD,EAAK,MAAM,GAAI,EACxBC,EAAK,CAAC,IAAM,OACdH,EAAQ,KAAKG,CAAI,CAErB,CACF,CACF,EACA,IAAMC,EAAO,IAAI,IACjB,OAAOC,EAAeL,CAAO,EAAE,OAAOM,GAElCA,EAAK,UAAYlB,GACjBkB,EAAK,KAAOjB,EAAO,OACnBiB,EAAK,OAASjB,EAAO,KACrBe,EAAK,IAAIE,EAAK,EAAE,EAET,IAETF,EAAK,IAAIE,EAAK,EAAE,EACT,GACR,CACH,CACF",
6
- "names": ["BUDGET_SLOTS", "WalkIndex", "file", "adapter", "WalkReader", "opts", "haplotypes", "refName", "region", "signal", "walkRows", "budgets", "graph", "kept", "fragments", "walksKept", "lane", "resolvePanSNPrefix", "walkCutFor", "refRows", "getLines", "line", "cols", "seen", "referenceNodes", "node"]
7
- }
@@ -1,254 +0,0 @@
1
- import {
2
- closingLinks,
3
- closingSpans,
4
- formatSubgraph,
5
- parseLinkLine,
6
- parseSegmentLine,
7
- segmentSamples,
8
- } from './rgfaBed.ts'
9
-
10
- import type { RgfaLink, RgfaSegment } from './rgfaBed.ts'
11
-
12
- // A five-column row is what `gfatools gfa2bed -m` writes for an rGFA. There is
13
- // no tag column at all, which is the backward-compatible case.
14
- const RGFA_SEG = 'K12#1#chr\t100\t150\ts1\t0'
15
-
16
- // A six-column row is what a path walk or a level-of-detail tier writes. The
17
- // sixth column is a space-separated GFA tag list.
18
- const TAGGED_SEG =
19
- 'GRCh38#0#chr1\t103611080\t103732636\ts10472\t1\t' +
20
- 'ct:Z:bubble cn:i:95 cw:i:269401 cs:i:26889 cl:i:316616 cv:i:1'
21
-
22
- function subgraphOf(segments: RgfaSegment[], links: RgfaLink[] = []) {
23
- return formatSubgraph(
24
- new Map(segments.map(s => [s.id, s])),
25
- new Map(links.map((l, i) => [String(i), l])),
26
- )
27
- }
28
-
29
- test('an rGFA row has no tags and synthesizes the S-line it always did', () => {
30
- const segment = parseSegmentLine(RGFA_SEG)
31
- expect(segment.tags).toBe('')
32
- expect(subgraphOf([segment])).toBe(
33
- ['H\tVN:Z:1.0', 'S\ts1\t*\tLN:i:50\tSN:Z:K12#1#chr\tSO:i:100\tSR:i:0'].join(
34
- '\n',
35
- ),
36
- )
37
- })
38
-
39
- // The hops' tabix queries run together and fill the map in arrival order
40
- test('segments at one position come out in the same order whichever arrived first', () => {
41
- const at = (id: string) => parseSegmentLine(`K12#1#chr\t100\t100\t${id}\t1`)
42
- expect(subgraphOf([at('s2'), at('s1')])).toBe(
43
- subgraphOf([at('s1'), at('s2')]),
44
- )
45
- })
46
-
47
- test('a tag column reaches the S-line as tab-separated GFA tags', () => {
48
- const segment = parseSegmentLine(TAGGED_SEG)
49
- expect(segment.tags).toBe(
50
- 'ct:Z:bubble cn:i:95 cw:i:269401 cs:i:26889 cl:i:316616 cv:i:1',
51
- )
52
- expect(subgraphOf([segment])).toBe(
53
- [
54
- 'H\tVN:Z:1.0',
55
- 'S\ts10472\t*\tLN:i:121556\tSN:Z:GRCh38#0#chr1\tSO:i:103611080\tSR:i:1' +
56
- '\tct:Z:bubble\tcn:i:95\tcw:i:269401\tcs:i:26889\tcl:i:316616\tcv:i:1',
57
- ].join('\n'),
58
- )
59
- })
60
-
61
- // A segment can enter a subgraph only through a link row, when the region
62
- // covers one endpoint and its neighbour sits elsewhere, so the endpoint records
63
- // carry tags too or a tier node reached that way would lose its summary.
64
- test('link rows carry each endpoint tags', () => {
65
- const link = parseLinkLine(
66
- [
67
- 'GRCh38#0#chr1',
68
- '0',
69
- '10',
70
- 's1+',
71
- 's2+',
72
- 'GRCh38#0#chr1',
73
- '0',
74
- '10',
75
- '0',
76
- 'GRCh38#0#chr1',
77
- '10',
78
- '20',
79
- '1',
80
- 'ct:Z:backbone',
81
- 'ct:Z:bubble cl:i:104730',
82
- ].join('\t'),
83
- )
84
- expect(link.sourceSegment.tags).toBe('ct:Z:backbone')
85
- expect(link.targetSegment.tags).toBe('ct:Z:bubble cl:i:104730')
86
- expect(subgraphOf([link.sourceSegment, link.targetSegment], [link])).toBe(
87
- [
88
- 'H\tVN:Z:1.0',
89
- 'S\ts1\t*\tLN:i:10\tSN:Z:GRCh38#0#chr1\tSO:i:0\tSR:i:0\tct:Z:backbone',
90
- 'S\ts2\t*\tLN:i:10\tSN:Z:GRCh38#0#chr1\tSO:i:10\tSR:i:1\tct:Z:bubble\tcl:i:104730',
91
- 'L\ts1\t+\ts2\t+\t0M',
92
- ].join('\n'),
93
- )
94
- })
95
-
96
- // An older six-column file from build_pggb_tabix.sh wrote a bare comma list
97
- // rather than a tag. It must not be emitted as a malformed GFA tag.
98
- test('a link row with no tag columns parses as untagged', () => {
99
- const link = parseLinkLine('c\t0\t10\ts1+\ts2+\tc\t0\t10\t0\tc\t10\t20\t1')
100
- expect(link.sourceSegment.tags).toBe('')
101
- expect(link.targetSegment.tags).toBe('')
102
- })
103
-
104
- // build_pggb_tabix.sh wrote a bare comma list in column 6 before the tag column
105
- // existed. Those files are still hosted, and a bare list is not a GFA tag.
106
- test('a legacy bare sample list is dropped rather than emitted as a tag', () => {
107
- const segment = parseSegmentLine('c\t0\t10\ts1\t1\tK12,Sakai,CFT073')
108
- expect(segment.tags).toBe('K12,Sakai,CFT073')
109
- expect(subgraphOf([segment])).toBe(
110
- ['H\tVN:Z:1.0', 'S\ts1\t*\tLN:i:10\tSN:Z:c\tSO:i:0\tSR:i:1'].join('\n'),
111
- )
112
- })
113
-
114
- test('a valid tag beside a malformed one keeps only the valid one', () => {
115
- const segment = parseSegmentLine('c\t0\t10\ts1\t1\tgarbage SM:Z:HG002.1')
116
- expect(subgraphOf([segment])).toBe(
117
- [
118
- 'H\tVN:Z:1.0',
119
- 'S\ts1\t*\tLN:i:10\tSN:Z:c\tSO:i:0\tSR:i:1\tSM:Z:HG002.1',
120
- ].join('\n'),
121
- )
122
- })
123
-
124
- test('segmentSamples reads the samples off the tag column', () => {
125
- const segment = parseSegmentLine(
126
- 'K12#1#chr\t1004477\t1004500\ts119690\t0\tSM:Z:K12.1,Sakai.1,NCTC86.1',
127
- )
128
- expect(segmentSamples(segment)).toEqual(['K12.1', 'Sakai.1', 'NCTC86.1'])
129
- })
130
-
131
- // Undefined, not [], so a lane can tell a graph that records no samples from a
132
- // segment no path visits.
133
- test('segmentSamples is undefined when the column says nothing', () => {
134
- expect(segmentSamples(parseSegmentLine(RGFA_SEG))).toBeUndefined()
135
- expect(segmentSamples(parseSegmentLine(TAGGED_SEG))).toBeUndefined()
136
- })
137
-
138
- // Same reason formatSegment checks the grammar: build_pggb_tabix.sh wrote a
139
- // bare comma list here before the tag column existed, and reading it would
140
- // invent a single sample named after the whole list.
141
- test('segmentSamples drops a legacy bare sample list', () => {
142
- const segment = parseSegmentLine('c\t0\t10\ts1\t1\tK12,Sakai,CFT073')
143
- expect(segmentSamples(segment)).toBeUndefined()
144
- })
145
-
146
- test('segmentSamples finds the tag beside other tags', () => {
147
- const segment = parseSegmentLine(
148
- 'c\t0\t10\ts1\t1\tct:Z:bubble SM:Z:HG002.1,HG002.2 cn:i:95',
149
- )
150
- expect(segmentSamples(segment)).toEqual(['HG002.1', 'HG002.2'])
151
- })
152
-
153
- // The whole samples chain in one test, because it spans three files and each
154
- // half is covered on its own: a segs.bed SM:Z: column -> the synthesized S-line
155
- // -> the GFA parser -> GraphNode.samples, which model.ts renders as `samples`
156
- // in the node popup. An indexed cut has no P/W lines, so this tag is its only
157
- // record of which haplotypes visit a segment, and nothing else asserts that it
158
- // survives the round trip.
159
- test('SM:Z: on a segs row reaches GraphNode.samples', async () => {
160
- const { parseGFA } = await import('@jbrowse/bandage-core/gfa-core/index')
161
- const { convertGFAToGraph } =
162
- await import('@jbrowse/bandage-core/gfa/gfaConverter')
163
- const segment = parseSegmentLine(
164
- 'K12#1#chr\t1004477\t1004500\ts119690\t0\tSM:Z:K12.1,Sakai.1,NCTC86.1',
165
- )
166
- const graph = convertGFAToGraph(parseGFA(subgraphOf([segment])))
167
- expect(graph.nodes[0]!.samples).toEqual(['K12.1', 'Sakai.1', 'NCTC86.1'])
168
- })
169
-
170
- // The C4 allele's shape on NA18948#2: one hop reached 352179 and 352180 from
171
- // the left end and 352183 and 352184 from the right, with 352181 and 352182
172
- // between them unreached.
173
- const NA18948 = 'NA18948#2#CM101588.1'
174
- const piece = (id: string, start: number, end: number): RgfaSegment => ({
175
- refName: NA18948,
176
- start,
177
- end,
178
- id,
179
- rank: 77,
180
- tags: '',
181
- })
182
- const reached = [
183
- piece('s352179', 32015241, 32036237),
184
- piece('s352180', 32036237, 32036352),
185
- piece('s352183', 32041508, 32047945),
186
- piece('s352184', 32047945, 32047979),
187
- ]
188
-
189
- const C4_WINDOW = { refName: 'GRCh38#0#chr6', start: 31979972, end: 32049972 }
190
-
191
- test('pieces of one stable sequence close as one span over the gap between them', () => {
192
- expect(closingSpans(reached, new Set(), C4_WINDOW)).toEqual([
193
- { refName: NA18948, start: 32015241, end: 32047979 },
194
- ])
195
- })
196
-
197
- test('a gap wider than the window splits the span, and read pieces that abut need nothing', () => {
198
- const narrow = { ...C4_WINDOW, end: C4_WINDOW.start + 1000 }
199
- expect(closingSpans(reached, new Set(), narrow)).toEqual([])
200
- expect(closingSpans(reached, new Set(['s352183']), narrow)).toEqual([
201
- { refName: NA18948, start: 32041508, end: 32047979 },
202
- ])
203
- })
204
-
205
- test('backbone past the window closes between abutting pieces only', () => {
206
- const backbone = (id: string, start: number, end: number): RgfaSegment => ({
207
- refName: C4_WINDOW.refName,
208
- start,
209
- end,
210
- id,
211
- rank: 0,
212
- tags: '',
213
- })
214
- const held = [
215
- backbone('s329760', 31932655, 31984683),
216
- backbone('s400000', 32049000, 32050100),
217
- backbone('s400001', 32050100, 32050200),
218
- backbone('s400005', 32060000, 32060100),
219
- ]
220
- expect(closingSpans(held, new Set(), C4_WINDOW)).toEqual([
221
- { refName: C4_WINDOW.refName, start: 32050100, end: 32050200 },
222
- { refName: C4_WINDOW.refName, start: 32060000, end: 32060100 },
223
- ])
224
- })
225
-
226
- test('a closing read keeps links between held segments and the pieces inside its span', () => {
227
- const held = new Map(reached.map(s => [s.id, s]))
228
- const link = (source: RgfaSegment, target: RgfaSegment): RgfaLink => ({
229
- source: source.id,
230
- sourceStrand: '+',
231
- target: target.id,
232
- targetStrand: '+',
233
- sourceSegment: source,
234
- targetSegment: target,
235
- })
236
- const s352181 = piece('s352181', 32036352, 32041501)
237
- const s352182 = piece('s352182', 32041501, 32041508)
238
- const elsewhere: RgfaSegment = {
239
- ...piece('s355491', 31985674, 31985801),
240
- refName: 'HG03688#1#JBHDTJ010000054.1',
241
- }
242
- const spans = closingSpans(reached, new Set(), C4_WINDOW)
243
- const found = [
244
- link(reached[1]!, s352181),
245
- link(s352181, s352182),
246
- link(s352182, reached[2]!),
247
- link(s352181, elsewhere),
248
- ]
249
- expect(closingLinks(found, held, spans).map(l => l.target)).toEqual([
250
- 's352181',
251
- 's352182',
252
- 's352183',
253
- ])
254
- })
@@ -1,314 +0,0 @@
1
- // A segment as it appears in segs.bed.gz, and as it is repeated inside every
2
- // links.bed.gz row: `stableName start end segmentId rank [tags]`. The first
3
- // five are the SN/SO/SR tags rGFA already carries, projected to BED by
4
- // `gfatools gfa2bed -m`.
5
- //
6
- // `tags` is a space-separated list of GFA tags, written verbatim onto the
7
- // S-line formatSegment synthesizes, where the GFA parser reads them into
8
- // `GraphNode.tags` with no work here. It is the extension point for everything
9
- // the five columns cannot say: carriage on a path-derived graph (`SM:Z:`), the
10
- // collapse summary on a level-of-detail tier (`ct:Z:`, `cn:i:`, ...), a
11
- // precomputed layout position later. rGFA files have no sixth column at all,
12
- // so this is empty for them and nothing changes.
13
- export interface RgfaSegment {
14
- refName: string
15
- start: number
16
- end: number
17
- id: string
18
- rank: number
19
- tags: string
20
- }
21
-
22
- export interface RgfaLink {
23
- source: string
24
- sourceStrand: string
25
- target: string
26
- targetStrand: string
27
- sourceSegment: RgfaSegment
28
- targetSegment: RgfaSegment
29
- }
30
-
31
- export function parseSegmentLine(line: string): RgfaSegment {
32
- return parseSegmentRow(line).segment
33
- }
34
-
35
- // gfa-to-tabix's anchored layout files a segment under the reference interval
36
- // its bubble hangs from and states the segment's own coordinate after the rank:
37
- // `anchorName anchorStart anchorEnd segmentId rank stableName start end [tags]`.
38
- // One query over a region then returns every segment of every bubble under it,
39
- // and `anchored` tells a cut it has nothing left to follow.
40
- export function parseSegmentRow(line: string) {
41
- const cols = line.split('\t')
42
- const anchored = cols.length >= 8
43
- const own = anchored ? 5 : 0
44
- return {
45
- anchored,
46
- segment: {
47
- refName: cols[own]!,
48
- start: +cols[own + 1]!,
49
- end: +cols[own + 2]!,
50
- id: cols[3]!,
51
- rank: +cols[4]!,
52
- tags: cols[anchored ? 8 : 5] ?? '',
53
- } satisfies RgfaSegment,
54
- }
55
- }
56
-
57
- // Carriage as an ordinary feature attribute, for the linear track rather than
58
- // the graph. The tag column reaches the graph view through the synthesized
59
- // S-line (formatSegment below, then gfaConverter reads SM into
60
- // `GraphNode.samples`), and that route ends at the graph: `getFeatures` builds
61
- // its own features and had no way to say who carries a segment, so a lane
62
- // colored by carriage was not expressible.
63
- //
64
- // `SM:Z:` is its own grammar check here. build_pggb_tabix.sh wrote a bare
65
- // comma-separated list in this column before the tag column existed, and those
66
- // files are still hosted; a bare list has no prefix to match, so it is dropped
67
- // rather than read as one sample named `K12,Sakai,CFT073`.
68
- //
69
- // Undefined rather than [] when the tag is absent, matching gfaConverter: an
70
- // rGFA has no sixth column at all, so a segment that says nothing about
71
- // carriage stays distinguishable from one carried by nobody.
72
- export function segmentSamples(segment: RgfaSegment) {
73
- const tag = segment.tags.split(' ').find(t => t.startsWith('SM:Z:'))
74
- if (tag === undefined) {
75
- return undefined
76
- }
77
- const samples = tag
78
- .slice('SM:Z:'.length)
79
- .split(',')
80
- .map(s => s.trim())
81
- .filter(Boolean)
82
- return samples.length > 0 ? samples : undefined
83
- }
84
-
85
- // `s322+` — the L-line's segment id with its orientation appended, mirroring
86
- // the GFA record it came from.
87
- function parseEndpoint(field: string) {
88
- return { id: field.slice(0, -1), strand: field.slice(-1) }
89
- }
90
-
91
- export function parseLinkLine(line: string): RgfaLink {
92
- const cols = line.split('\t')
93
- const source = parseEndpoint(cols[3]!)
94
- const target = parseEndpoint(cols[4]!)
95
- return {
96
- source: source.id,
97
- sourceStrand: source.strand,
98
- target: target.id,
99
- targetStrand: target.strand,
100
- sourceSegment: {
101
- id: source.id,
102
- refName: cols[5]!,
103
- start: +cols[6]!,
104
- end: +cols[7]!,
105
- rank: +cols[8]!,
106
- tags: cols[13] ?? '',
107
- },
108
- targetSegment: {
109
- id: target.id,
110
- refName: cols[9]!,
111
- start: +cols[10]!,
112
- end: +cols[11]!,
113
- rank: +cols[12]!,
114
- tags: cols[14] ?? '',
115
- },
116
- }
117
- }
118
-
119
- export function linkKey(link: RgfaLink) {
120
- return `${link.source}${link.sourceStrand}${link.target}${link.targetStrand}`
121
- }
122
-
123
- // gfa-to-tabix writes both endpoints of a link into every row and files the
124
- // link under each endpoint's stable sequence, so a read covering both returns
125
- // it twice; linkKey dedupes it. Returns the endpoints the cut lacked.
126
- export function addLink(
127
- segments: Map<string, RgfaSegment>,
128
- links: Map<string, RgfaLink>,
129
- link: RgfaLink,
130
- ) {
131
- links.set(linkKey(link), link)
132
- const added: RgfaSegment[] = []
133
- for (const segment of [link.sourceSegment, link.targetSegment]) {
134
- if (!segments.has(segment.id)) {
135
- segments.set(segment.id, segment)
136
- added.push(segment)
137
- }
138
- }
139
- return added
140
- }
141
-
142
- // The cut an anchored index gives: `under` is what the segment file returned
143
- // for the window, every segment of every bubble under it, and `found` is what
144
- // the link file returned. gfa-to-tabix files a link widely enough that `found`
145
- // holds every link touching a segment in `under` and every link between the
146
- // segments those lead to, plus links of neither kind, which are dropped here.
147
- // A backbone link that jumps the whole window, as a deletion or inversion
148
- // spanning it does, touches nothing in `under` and is kept by its own rule.
149
- export function anchoredCut(
150
- under: Map<string, RgfaSegment>,
151
- found: RgfaLink[],
152
- window: StableSpan,
153
- ) {
154
- const side = (segment: RgfaSegment) =>
155
- segment.rank !== 0 || segment.refName !== window.refName
156
- ? 0
157
- : segment.end <= window.start
158
- ? -1
159
- : segment.start >= window.end
160
- ? 1
161
- : 0
162
- const segments = new Map(under)
163
- const links = new Map<string, RgfaLink>()
164
- for (const link of found) {
165
- if (
166
- under.has(link.source) ||
167
- under.has(link.target) ||
168
- side(link.sourceSegment) * side(link.targetSegment) === -1
169
- ) {
170
- addLink(segments, links, link)
171
- }
172
- }
173
- for (const link of found) {
174
- if (segments.has(link.source) && segments.has(link.target)) {
175
- links.set(linkKey(link), link)
176
- }
177
- }
178
- return { segments, links }
179
- }
180
-
181
- export interface StableSpan {
182
- refName: string
183
- start: number
184
- end: number
185
- }
186
-
187
- // Where a cut reads links once more to close itself. A hop reads the links of
188
- // the segments the round before it reached, so a link between two segments of
189
- // the last round is never read and an allele reached from both ends draws as
190
- // two stubs. rGFA lays an allele out as one stretch of the stable sequence that
191
- // introduced it, so pieces of one sequence within the window's length of each
192
- // other merge into a span, and reading it brings the pieces between too.
193
- // Backbone reached past the window is unread for the same reason; its spans
194
- // join only abutting pieces, since the reference past the window is not the
195
- // cut's to fill. A span whose segments were all read and abut is skipped.
196
- export function closingSpans(
197
- held: RgfaSegment[],
198
- unread: Set<string>,
199
- window: StableSpan,
200
- ) {
201
- const outside = (segment: RgfaSegment) =>
202
- segment.refName !== window.refName ||
203
- segment.end <= window.start ||
204
- segment.start >= window.end
205
- return [
206
- ...mergedSpans(
207
- held.filter(segment => segment.rank > 0),
208
- segment => unread.has(segment.id),
209
- window.end - window.start,
210
- ),
211
- ...mergedSpans(
212
- held.filter(segment => segment.rank === 0 && outside(segment)),
213
- () => true,
214
- 0,
215
- ),
216
- ]
217
- }
218
-
219
- function mergedSpans(
220
- segments: RgfaSegment[],
221
- isUnread: (segment: RgfaSegment) => boolean,
222
- maxGap: number,
223
- ) {
224
- const sorted = [...segments].sort(
225
- (a, b) => compare(a.refName, b.refName) || a.start - b.start,
226
- )
227
- const spans: (StableSpan & { needed: boolean })[] = []
228
- for (const segment of sorted) {
229
- const span = spans.at(-1)
230
- if (
231
- span?.refName === segment.refName &&
232
- segment.start - span.end <= maxGap
233
- ) {
234
- span.needed ||= segment.start > span.end || isUnread(segment)
235
- span.end = Math.max(span.end, segment.end)
236
- } else {
237
- spans.push({
238
- refName: segment.refName,
239
- start: segment.start,
240
- end: segment.end,
241
- needed: isUnread(segment),
242
- })
243
- }
244
- }
245
- return spans
246
- .filter(span => span.needed)
247
- .map(({ refName, start, end }): StableSpan => ({ refName, start, end }))
248
- }
249
-
250
- // The links a closing read keeps: each end is a segment the cut holds or one
251
- // lying inside a span. Following anything else is what another hop is for.
252
- export function closingLinks(
253
- found: RgfaLink[],
254
- held: Map<string, RgfaSegment>,
255
- spans: StableSpan[],
256
- ) {
257
- const inside = (segment: RgfaSegment) =>
258
- held.has(segment.id) ||
259
- spans.some(
260
- span =>
261
- span.refName === segment.refName &&
262
- segment.start >= span.start &&
263
- segment.end <= span.end,
264
- )
265
- return found.filter(
266
- link => inside(link.sourceSegment) && inside(link.targetSegment),
267
- )
268
- }
269
-
270
- // Segments carry no sequence here — the BED records only their span — so every
271
- // S-line is written with `*` and an LN tag, which is what the GFA spec asks for
272
- // and what packages/graph-core's parser reads back as the node length.
273
- // `NM:i:0`, `SM:Z:HG002.1,HG002.2` — the GFA tag grammar, checked rather than
274
- // trusted. Files built before the tag column existed put a bare comma-separated
275
- // sample list here, and passing that through would put a field that is not a
276
- // tag on an S-line, which is a malformed GFA rather than a missing annotation.
277
- // Dropping it degrades to the pre-tag behaviour instead.
278
- const GFA_TAG = /^[A-Za-z][A-Za-z0-9]:[AifZJHB]:/
279
-
280
- function formatSegment(segment: RgfaSegment) {
281
- const { id, refName, start, end, rank, tags } = segment
282
- const valid = tags.split(' ').filter(t => GFA_TAG.test(t))
283
- const extra = valid.length === 0 ? '' : `\t${valid.join('\t')}`
284
- return `S\t${id}\t*\tLN:i:${end - start}\tSN:Z:${refName}\tSO:i:${start}\tSR:i:${rank}${extra}`
285
- }
286
-
287
- function formatLink(link: RgfaLink) {
288
- const { source, sourceStrand, target, targetStrand } = link
289
- return `L\t${source}\t${sourceStrand}\t${target}\t${targetStrand}\t0M`
290
- }
291
-
292
- // Codepoint order, not localeCompare: the byte-identical-output guarantee below
293
- // has to hold across locales.
294
- function compare(a: string, b: string) {
295
- return a < b ? -1 : a > b ? 1 : 0
296
- }
297
-
298
- // Deterministic ordering: the same region always produces byte-identical GFA,
299
- // so a layout computed from it is reproducible.
300
- export function formatSubgraph(
301
- segments: Map<string, RgfaSegment>,
302
- links: Map<string, RgfaLink>,
303
- ) {
304
- const sortedSegments = [...segments.values()].sort(
305
- (a, b) =>
306
- compare(a.refName, b.refName) || a.start - b.start || compare(a.id, b.id),
307
- )
308
- const sortedLinks = [...links.keys()].sort((a, b) => compare(a, b))
309
- return [
310
- 'H\tVN:Z:1.0',
311
- ...sortedSegments.map(s => formatSegment(s)),
312
- ...sortedLinks.map(k => formatLink(links.get(k)!)),
313
- ].join('\n')
314
- }