@jbrowse/graph-walks 1.0.0 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -27,11 +27,7 @@ npm install @jbrowse/graph-walks @gmod/tabix
27
27
 
28
28
  ```ts
29
29
  import { TabixIndexedFile } from '@gmod/tabix'
30
- import {
31
- WalkReader,
32
- panSNHaplotype,
33
- walkNameFilter,
34
- } from '@jbrowse/graph-walks'
30
+ import { WalkReader, walksKept } from '@jbrowse/graph-walks'
35
31
  import { RemoteFile } from 'generic-filehandle2'
36
32
 
37
33
  const url = 'https://example.org/hprc-v2.1-mc-grch38.GRCh38.walks.bed.gz'
@@ -46,7 +42,7 @@ const refName = 'GRCh38#0#chr6'
46
42
  const { graph, kept, fragments } = await reader.cut(
47
43
  refName,
48
44
  { start: 160_614_798, end: 160_647_758 },
49
- { keep: walkNameFilter(['HG00097', 'HG00133'], panSNHaplotype(refName)) },
45
+ { keep: walksKept(['HG00097', 'HG00133'], refName) },
50
46
  )
51
47
  const tables = graph.tables(kept, fragments)
52
48
  ```
@@ -73,6 +69,18 @@ const tables = graph.tables(kept, fragments)
73
69
 
74
70
  A cut over a budget throws a `RegionTooLargeError`. Its `fitsBp` holds about how
75
71
  wide a window would fit, and JBrowse shows it as a zoom-in notice.
72
+ `WALK_BUDGETS` holds the step and byte budgets the JBrowse plugin and BandageJS
73
+ cut by.
74
+
75
+ ## Naming the window
76
+
77
+ - `reader.sequenceFor(sample, refName)` gives the walk file's sequence for a
78
+ contig on a sample: `chr6` on `GRCh38` is `GRCh38#0#chr6`, and a pggb file's
79
+ `chm13#chr1` is `chr1` on `chm13`
80
+ - `walksKept(haplotypes, refName)` is the `keep` filter for a cut on `refName`:
81
+ its own walk and those of the haplotypes named
82
+ - `refNameLookup` and `resolveRefName` resolve a contig the same way against any
83
+ tabix file's sequence names
76
84
 
77
85
  ## How a cut works
78
86
 
package/esm/index.d.ts CHANGED
@@ -1,7 +1,8 @@
1
- export { BUDGET_NAMES, WalkGraph, byteBudgetError, chunkQueryStart, joinPieces, keptStepsError, lookbackChunks, stepBudgetError, walkCut, walkHeader, walkNameFilter, } from './walkRows.ts';
1
+ export { BUDGET_NAMES, WalkGraph, byteBudgetError, chunkQueryStart, joinPieces, keptStepsError, lookbackChunks, stepBudgetError, walkCut, walkHeader, walkNameFilter, walksKept, } from './walkRows.ts';
2
2
  export type { BudgetNames, WalkChunk, WalkFragment, WalkHeader, WalkNode, WalkRow, WalkRun, WalkWindow, } from './walkRows.ts';
3
- export { WalkFormatError, WalkReader } from './walkReader.ts';
3
+ export { WALK_BUDGETS, WalkFormatError, WalkReader, checkRange, } from './walkReader.ts';
4
+ export { refNameLookup, resolveRefName } from './refNames.ts';
4
5
  export { referenceNodes } from './walkChunk.ts';
5
6
  export type { TabixSource, WalkCutOptions, WalkReaderHeader, } from './walkReader.ts';
6
- export { RegionTooLargeError, bpString, panSNHaplotype, panSNMatchesPrefix, } from './util.ts';
7
+ export { RegionTooLargeError, bpString, panSNContig, panSNHaplotype, panSNMatchesPrefix, panSNSample, } from './util.ts';
7
8
  export type { GraphTables } from './util.ts';
package/esm/index.js CHANGED
@@ -1,4 +1,5 @@
1
- export { BUDGET_NAMES, WalkGraph, byteBudgetError, chunkQueryStart, joinPieces, keptStepsError, lookbackChunks, stepBudgetError, walkCut, walkHeader, walkNameFilter, } from "./walkRows.js";
2
- export { WalkFormatError, WalkReader } from "./walkReader.js";
1
+ export { BUDGET_NAMES, WalkGraph, byteBudgetError, chunkQueryStart, joinPieces, keptStepsError, lookbackChunks, stepBudgetError, walkCut, walkHeader, walkNameFilter, walksKept, } from "./walkRows.js";
2
+ export { WALK_BUDGETS, WalkFormatError, WalkReader, checkRange, } from "./walkReader.js";
3
+ export { refNameLookup, resolveRefName } from "./refNames.js";
3
4
  export { referenceNodes } from "./walkChunk.js";
4
- export { RegionTooLargeError, bpString, panSNHaplotype, panSNMatchesPrefix, } from "./util.js";
5
+ export { RegionTooLargeError, bpString, panSNContig, panSNHaplotype, panSNMatchesPrefix, panSNSample, } from "./util.js";
@@ -0,0 +1,12 @@
1
+ /**
2
+ * A file's sequence names under each spelling a caller may ask by: the file's
3
+ * own (`GRCh38#0#chr6`), and a PanSN name's contig under its sample (`chr6`
4
+ * on `GRCh38`) and under its haplotype (`chr6` on `GRCh38#0`). A pggb name
5
+ * with no haplotype field, `chm13#chr1`, is `chr1` on `chm13`.
6
+ */
7
+ export declare function refNameLookup(names: string[]): Map<string, string>;
8
+ /**
9
+ * The file's name for `refName` on `assemblyName`, a sample or a haplotype,
10
+ * else for `refName` as the file spells it; undefined when it holds neither
11
+ */
12
+ export declare function resolveRefName(lookup: Map<string, string>, assemblyName: string, refName: string): string | undefined;
@@ -0,0 +1,35 @@
1
+ import { panSNContig, panSNHaplotype, panSNSample } from "./util.js";
2
+ // A tab can occur in neither half: both come out of BED columns
3
+ function qualifiedKey(assemblyName, refName) {
4
+ return `${assemblyName}\t${refName}`;
5
+ }
6
+ /**
7
+ * A file's sequence names under each spelling a caller may ask by: the file's
8
+ * own (`GRCh38#0#chr6`), and a PanSN name's contig under its sample (`chr6`
9
+ * on `GRCh38`) and under its haplotype (`chr6` on `GRCh38#0`). A pggb name
10
+ * with no haplotype field, `chm13#chr1`, is `chr1` on `chm13`.
11
+ */
12
+ export function refNameLookup(names) {
13
+ const lookup = new Map();
14
+ for (const name of names) {
15
+ lookup.set(name, name);
16
+ const sample = panSNSample(name);
17
+ if (sample !== name) {
18
+ const contig = panSNContig(name);
19
+ lookup.set(qualifiedKey(sample, contig), name);
20
+ const haplotype = panSNHaplotype(name);
21
+ if (haplotype !== undefined) {
22
+ lookup.set(qualifiedKey(haplotype, contig), name);
23
+ }
24
+ }
25
+ }
26
+ return lookup;
27
+ }
28
+ /**
29
+ * The file's name for `refName` on `assemblyName`, a sample or a haplotype,
30
+ * else for `refName` as the file spells it; undefined when it holds neither
31
+ */
32
+ export function resolveRefName(lookup, assemblyName, refName) {
33
+ var _a;
34
+ return (_a = lookup.get(qualifiedKey(assemblyName, refName))) !== null && _a !== void 0 ? _a : lookup.get(refName);
35
+ }
package/esm/util.d.ts CHANGED
@@ -34,6 +34,8 @@ export declare class RegionTooLargeError extends Error {
34
34
  readonly fitsBp: number;
35
35
  constructor(message: string, limit: number, windowBp: number, fitsBp: number);
36
36
  }
37
+ export declare function panSNSample(refName: string): string;
38
+ export declare function panSNContig(refName: string): string;
37
39
  export declare function panSNHaplotype(refName: string): string | undefined;
38
40
  export declare function panSNMatchesPrefix(refName: string, prefix: string | undefined): boolean;
39
41
  export declare function bpString(bp: number): string;
package/esm/util.js CHANGED
@@ -14,6 +14,19 @@ export class RegionTooLargeError extends Error {
14
14
  }
15
15
  }
16
16
  const SEP = '#';
17
+ // `grape#1#chr1` -> `grape`
18
+ export function panSNSample(refName) {
19
+ return refName.split(SEP)[0];
20
+ }
21
+ // `grape#1#chr1` and `grape#chr1` -> `chr1`
22
+ export function panSNContig(refName) {
23
+ const parts = refName.split(SEP);
24
+ return parts.length >= 3
25
+ ? parts.slice(2).join(SEP)
26
+ : parts.length === 2
27
+ ? parts[1]
28
+ : refName;
29
+ }
17
30
  export function panSNHaplotype(refName) {
18
31
  const parts = refName.split(SEP);
19
32
  return parts.length >= 3 ? `${parts[0]}${SEP}${parts[1]}` : undefined;
@@ -1,5 +1,9 @@
1
1
  import { WalkGraph } from './walkRows.ts';
2
2
  import type { BudgetNames, WalkHeader } from './walkRows.ts';
3
+ export declare const WALK_BUDGETS: {
4
+ stepBudget: number;
5
+ byteBudget: number;
6
+ };
3
7
  /**
4
8
  * A Tabix-indexed file as the reader uses one: @gmod/tabix's
5
9
  * TabixIndexedFile is one. `bytesForRegions` estimates the compressed bytes a
@@ -27,6 +31,7 @@ export interface TabixSource {
27
31
  export declare class WalkFormatError extends Error {
28
32
  name: string;
29
33
  }
34
+ export declare function checkRange(refName: string, start: number, end: number): void;
30
35
  export interface WalkCutOptions {
31
36
  signal?: AbortSignal;
32
37
  keep?: (name: string) => boolean;
@@ -52,6 +57,7 @@ export type WalkReaderHeader = WalkHeader & {
52
57
  */
53
58
  export declare class WalkReader {
54
59
  private headerRead;
60
+ private lookupRead;
55
61
  private readonly walks;
56
62
  constructor(walks: TabixSource);
57
63
  header(opts?: {
@@ -60,6 +66,14 @@ export declare class WalkReader {
60
66
  referenceSequences(opts?: {
61
67
  signal?: AbortSignal;
62
68
  }): Promise<string[]>;
69
+ /**
70
+ * The walk file's sequence for `refName` on `sample`, or for `refName` as
71
+ * the file spells it: `chr6` on `GRCh38` is `GRCh38#0#chr6`. Undefined when
72
+ * the file holds neither.
73
+ */
74
+ sequenceFor(sample: string, refName: string, opts?: {
75
+ signal?: AbortSignal;
76
+ }): Promise<string | undefined>;
63
77
  queryStart(start: number, opts?: {
64
78
  signal?: AbortSignal;
65
79
  }): Promise<number>;
package/esm/walkReader.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { refNameLookup, resolveRefName } from "./refNames.js";
1
2
  import { decodeChunk, pieceLinks } from "./walkChunk.js";
2
3
  import { WalkGraph, byteBudgetError, chunkQueryStart, joinPieces, keptStepsError, lookbackChunks, stepBudgetError, walkCut, walkHeader, } from "./walkRows.js";
3
4
  // A window reads whole chunks and keeps the steps on it, so a narrow window
@@ -12,6 +13,8 @@ const FOLLOW_ROUNDS = 3;
12
13
  const FOLLOW_CHUNKS = 4;
13
14
  // bp of reference a cut keeps past each end of the window
14
15
  const WALK_CONTEXT = 1000;
16
+ // The budgets the JBrowse graph plugin and BandageJS cut a browser's window by
17
+ export const WALK_BUDGETS = { stepBudget: 4000000, byteBudget: 8000000 };
15
18
  // A walk file gfa-to-tabix 0.12 or earlier wrote, which the reader refuses
16
19
  export class WalkFormatError extends Error {
17
20
  constructor() {
@@ -21,7 +24,7 @@ export class WalkFormatError extends Error {
21
24
  }
22
25
  // @gmod/tabix loops forever on a NaN end and reads nothing for a NaN start
23
26
  // (GMOD/tabix-js#157), so every query refuses a range that is not finite first
24
- function checkRange(refName, start, end) {
27
+ export function checkRange(refName, start, end) {
25
28
  if (!Number.isFinite(start) || !Number.isFinite(end)) {
26
29
  throw new Error(`${refName}:${start}-${end} is not a finite range to query the graph's index for`);
27
30
  }
@@ -87,6 +90,24 @@ export class WalkReader {
87
90
  referenceSequences(opts = {}) {
88
91
  return this.walks.getReferenceSequenceNames(opts);
89
92
  }
93
+ /**
94
+ * The walk file's sequence for `refName` on `sample`, or for `refName` as
95
+ * the file spells it: `chr6` on `GRCh38` is `GRCh38#0#chr6`. Undefined when
96
+ * the file holds neither.
97
+ */
98
+ async sequenceFor(sample, refName, opts = {}) {
99
+ if (!this.lookupRead) {
100
+ const read = this.walks.getReferenceSequenceNames().then(refNameLookup);
101
+ read.catch(() => {
102
+ if (this.lookupRead === read) {
103
+ this.lookupRead = undefined;
104
+ }
105
+ });
106
+ this.lookupRead = read;
107
+ }
108
+ const lookup = await raceAbort(this.lookupRead, opts.signal);
109
+ return resolveRefName(lookup, sample, refName);
110
+ }
90
111
  async queryStart(start, opts = {}) {
91
112
  const { chunk, lookback } = await this.header(opts);
92
113
  return chunkQueryStart(start, chunk, lookback);
package/esm/walkRows.d.ts CHANGED
@@ -58,6 +58,12 @@ export declare function walkHeader(lines: string[]): WalkHeader;
58
58
  export declare function lookbackChunks(maxNode: number | undefined, chunk: number): number;
59
59
  export declare function chunkQueryStart(start: number, chunk: number, lookback?: number): number;
60
60
  export declare function walkNameFilter(wanted: string[] | undefined, referenceHaplotype: string | undefined): ((name: string) => boolean) | undefined;
61
+ /**
62
+ * The walks a cut on `refName` decodes: its own and those `wanted` names. A
63
+ * pggb P-line names a reference with no haplotype field (`chm13#chr1`), so the
64
+ * filter matches the reference by its whole name.
65
+ */
66
+ export declare function walksKept(wanted: string[] | undefined, refName: string): ((name: string) => boolean) | undefined;
61
67
  export interface WalkRun {
62
68
  name: string;
63
69
  hapOffset: number;
package/esm/walkRows.js CHANGED
@@ -1,4 +1,4 @@
1
- import { RegionTooLargeError, bpString, panSNMatchesPrefix } from "./util.js";
1
+ import { RegionTooLargeError, bpString, panSNHaplotype, panSNMatchesPrefix, } from "./util.js";
2
2
  import { placeAndLink } from "./walkChunk.js";
3
3
  function integerTag(line, tag) {
4
4
  const match = new RegExp(`(?:^|\\t)${tag}:i:(\\d+)`).exec(line);
@@ -53,6 +53,15 @@ export function walkNameFilter(wanted, referenceHaplotype) {
53
53
  : (name) => panSNMatchesPrefix(name, referenceHaplotype) ||
54
54
  wanted.some(prefix => panSNMatchesPrefix(name, prefix));
55
55
  }
56
+ /**
57
+ * The walks a cut on `refName` decodes: its own and those `wanted` names. A
58
+ * pggb P-line names a reference with no haplotype field (`chm13#chr1`), so the
59
+ * filter matches the reference by its whole name.
60
+ */
61
+ export function walksKept(wanted, refName) {
62
+ var _a;
63
+ return walkNameFilter(wanted, (_a = panSNHaplotype(refName)) !== null && _a !== void 0 ? _a : refName);
64
+ }
56
65
  export function joinPieces(rows) {
57
66
  const byPath = new Map();
58
67
  for (const row of rows) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jbrowse/graph-walks",
3
- "version": "1.0.0",
3
+ "version": "1.1.0",
4
4
  "description": "Read gfa-to-tabix walk files by range requests and cut a window of a pangenome graph: the reference, every haplotype's walk through it, and the nodes and links they visit",
5
5
  "license": "MIT",
6
6
  "author": {