jbrowse-plugin-graphgenomeviewer 7.2.0 → 7.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/chunks/GbzBaseSyntenyAdapter-FXMGIM7Z.js +2 -0
- package/dist/chunks/{GbzBaseSyntenyAdapter-NB6WBBZH.js.map → GbzBaseSyntenyAdapter-FXMGIM7Z.js.map} +3 -3
- package/dist/chunks/{GraphGenomeView-4O5F5I37.js → GraphGenomeView-WJ3FIDY2.js} +2 -2
- package/dist/chunks/{GraphTrackSettingsDialog-WZEJAF7F.js → GraphTrackSettingsDialog-ZW7ALMDV.js} +2 -2
- package/dist/chunks/{HighlightColorDialog-SRT2FPXP.js → HighlightColorDialog-VBZRUKD5.js} +2 -2
- package/dist/chunks/{LinearGraphDisplay-Z33PVCJL.js → LinearGraphDisplay-SPOPTJEN.js} +2 -2
- package/dist/chunks/MinigraphBubbleAdapter-QCOMFPLL.js +2 -0
- package/dist/chunks/{MinigraphBubbleAdapter-HDLUZ6YF.js.map → MinigraphBubbleAdapter-QCOMFPLL.js.map} +1 -1
- package/dist/chunks/RgfaTabixAdapter-O3NMNRZ2.js +2 -0
- package/dist/chunks/RgfaTabixAdapter-O3NMNRZ2.js.map +7 -0
- package/dist/chunks/WalkTabixSyntenyAdapter-I3PY6HJ2.js +2 -0
- package/dist/chunks/{WalkTabixSyntenyAdapter-WBHOZLAN.js.map → WalkTabixSyntenyAdapter-I3PY6HJ2.js.map} +1 -1
- package/dist/chunks/chunk-2M4XYSLG.js +2 -0
- package/dist/chunks/chunk-2M4XYSLG.js.map +7 -0
- package/dist/chunks/{chunk-JLGPQO5L.js → chunk-2O3YVA2I.js} +2 -2
- package/dist/chunks/{chunk-CWNJR5XB.js → chunk-44CJ2N3N.js} +1 -1
- package/dist/chunks/{chunk-CWNJR5XB.js.map → chunk-44CJ2N3N.js.map} +1 -1
- package/dist/chunks/chunk-6KUHGRHU.js +2 -0
- package/dist/chunks/chunk-6KUHGRHU.js.map +7 -0
- package/dist/chunks/chunk-6MUBJVBG.js +2 -0
- package/dist/chunks/chunk-6MUBJVBG.js.map +7 -0
- package/dist/chunks/{chunk-GUXZESDR.js → chunk-7HWHWPRW.js} +1 -1
- package/dist/chunks/{chunk-GUXZESDR.js.map → chunk-7HWHWPRW.js.map} +1 -1
- package/dist/chunks/chunk-B2EP6FOQ.js +3 -0
- package/dist/chunks/chunk-B2EP6FOQ.js.map +7 -0
- package/dist/chunks/{chunk-44HZPYGC.js → chunk-BXRTG7FU.js} +2 -2
- package/dist/chunks/{chunk-44HZPYGC.js.map → chunk-BXRTG7FU.js.map} +1 -1
- package/dist/chunks/{chunk-HLKZCTQ3.js → chunk-DBPEUVUC.js} +1 -1
- package/dist/chunks/{chunk-HLKZCTQ3.js.map → chunk-DBPEUVUC.js.map} +1 -1
- package/dist/chunks/{chunk-FME624X3.js → chunk-DUR437TH.js} +2 -2
- package/dist/chunks/{chunk-FME624X3.js.map → chunk-DUR437TH.js.map} +1 -1
- package/dist/chunks/{chunk-6WA6WF6A.js → chunk-FIRQAKMX.js} +1 -1
- package/dist/chunks/{chunk-6WA6WF6A.js.map → chunk-FIRQAKMX.js.map} +1 -1
- package/dist/chunks/chunk-JCAZTNYG.js +2 -0
- package/dist/chunks/chunk-JCAZTNYG.js.map +7 -0
- package/dist/chunks/chunk-N74HGPAW.js +3 -0
- package/dist/chunks/chunk-N74HGPAW.js.map +7 -0
- package/dist/chunks/chunk-PEDWEPNK.js +2 -0
- package/dist/chunks/chunk-PEDWEPNK.js.map +7 -0
- package/dist/chunks/chunk-PYQG3JHP.js +3 -0
- package/dist/chunks/chunk-PYQG3JHP.js.map +7 -0
- package/dist/chunks/chunk-RFDSEB2X.js +4 -0
- package/dist/chunks/chunk-RFDSEB2X.js.map +7 -0
- package/dist/chunks/{chunk-O7Y7LYN6.js → chunk-RW775LQN.js} +2 -2
- package/dist/chunks/{chunk-O7Y7LYN6.js.map → chunk-RW775LQN.js.map} +1 -1
- package/dist/chunks/{chunk-N4WBX2OS.js → chunk-VQ77X5QU.js} +2 -2
- package/dist/chunks/{chunk-N4WBX2OS.js.map → chunk-VQ77X5QU.js.map} +1 -1
- package/dist/chunks/{chunk-LXCMWL7B.js → chunk-WUSKVEA6.js} +2 -2
- package/dist/chunks/{chunk-LXCMWL7B.js.map → chunk-WUSKVEA6.js.map} +1 -1
- package/dist/chunks/{chunk-ZNT7TQLN.js → chunk-YKSUKPYY.js} +2 -2
- package/dist/chunks/chunk-YKSUKPYY.js.map +7 -0
- package/dist/chunks/{renderSvg-RRWCAEGF.js → renderSvg-WD6YDV2I.js} +2 -2
- package/dist/jbrowse-plugin-graphgenomeviewer.esm.js +4 -4
- package/dist/jbrowse-plugin-graphgenomeviewer.esm.js.map +4 -4
- package/package.json +3 -3
- package/src/GbzBaseSyntenyAdapter/GbzBaseSyntenyAdapter.test.ts +2 -2
- package/src/GbzBaseSyntenyAdapter/GbzBaseSyntenyAdapter.ts +1 -1
- package/src/GetSubgraph.ts +1 -1
- package/src/GraphGenomeView/genes/genes.test.ts +20 -2
- package/src/GraphGenomeView/legendSections.test.ts +1 -1
- package/src/GraphGenomeView/legendSections.ts +1 -7
- package/src/GraphGenomeView/model.test.ts +1 -2
- package/src/GraphGenomeView/pane/graphViews.ts +24 -35
- package/src/GraphGenomeView/pane/loadActions.ts +4 -3
- package/src/GraphGenomeView/pane/paneBase.ts +1 -1
- package/src/GraphGenomeView/subgraphLoad.test.ts +41 -1
- package/src/GraphGenomeView/walkRowsCut.ts +10 -44
- package/src/RgfaTabixAdapter/RgfaTabixAdapter.test.ts +23 -4
- package/src/RgfaTabixAdapter/RgfaTabixAdapter.ts +2 -3
- package/src/RgfaTabixAdapter/bubbleTier.test.ts +1 -1
- package/src/RgfaTabixAdapter/walkCut.ts +20 -0
- package/src/RgfaTabixAdapter/walkFiles.test.ts +2 -2
- package/src/RgfaTabixAdapter/walkReader.ts +1 -1
- package/src/WalkTabixSyntenyAdapter/WalkTabixSyntenyAdapter.test.ts +1 -1
- package/src/version.ts +1 -1
- package/dist/chunks/GbzBaseSyntenyAdapter-NB6WBBZH.js +0 -2
- package/dist/chunks/MinigraphBubbleAdapter-HDLUZ6YF.js +0 -2
- package/dist/chunks/RgfaTabixAdapter-XZRTWQMZ.js +0 -3
- package/dist/chunks/RgfaTabixAdapter-XZRTWQMZ.js.map +0 -7
- package/dist/chunks/WalkTabixSyntenyAdapter-WBHOZLAN.js +0 -2
- package/dist/chunks/chunk-BWP3XYKD.js +0 -2
- package/dist/chunks/chunk-BWP3XYKD.js.map +0 -7
- package/dist/chunks/chunk-EDSIYH4H.js +0 -2
- package/dist/chunks/chunk-EDSIYH4H.js.map +0 -7
- package/dist/chunks/chunk-III4GT4G.js +0 -2
- package/dist/chunks/chunk-III4GT4G.js.map +0 -7
- package/dist/chunks/chunk-N5GYTUIL.js +0 -3
- package/dist/chunks/chunk-N5GYTUIL.js.map +0 -7
- package/dist/chunks/chunk-PA4HKAUJ.js +0 -2
- package/dist/chunks/chunk-PA4HKAUJ.js.map +0 -7
- package/dist/chunks/chunk-PFAHNHMM.js +0 -2
- package/dist/chunks/chunk-PFAHNHMM.js.map +0 -7
- package/dist/chunks/chunk-UFQTIWSN.js +0 -4
- package/dist/chunks/chunk-UFQTIWSN.js.map +0 -7
- package/dist/chunks/chunk-VDHDEEMF.js +0 -2
- package/dist/chunks/chunk-VDHDEEMF.js.map +0 -7
- package/dist/chunks/chunk-WGVATZLM.js +0 -3
- package/dist/chunks/chunk-WGVATZLM.js.map +0 -7
- package/dist/chunks/chunk-ZNT7TQLN.js.map +0 -7
- package/src/RgfaTabixAdapter/rgfaBed.test.ts +0 -254
- package/src/RgfaTabixAdapter/rgfaBed.ts +0 -314
- package/src/RgfaTabixAdapter/rgfaCut.ts +0 -139
- package/src/RgfaTabixAdapter/walkRowRuns.test.ts +0 -192
- package/src/RgfaTabixAdapter/walkRowRuns.ts +0 -520
- /package/dist/chunks/{GraphGenomeView-4O5F5I37.js.map → GraphGenomeView-WJ3FIDY2.js.map} +0 -0
- /package/dist/chunks/{GraphTrackSettingsDialog-WZEJAF7F.js.map → GraphTrackSettingsDialog-ZW7ALMDV.js.map} +0 -0
- /package/dist/chunks/{HighlightColorDialog-SRT2FPXP.js.map → HighlightColorDialog-VBZRUKD5.js.map} +0 -0
- /package/dist/chunks/{LinearGraphDisplay-Z33PVCJL.js.map → LinearGraphDisplay-SPOPTJEN.js.map} +0 -0
- /package/dist/chunks/{chunk-JLGPQO5L.js.map → chunk-2O3YVA2I.js.map} +0 -0
- /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
|
-
}
|