Repository navigation
Added
- Released together with progeny-selector (https://github.com/piercetaylor/progeny-selector/releases/tag/v0.1.0); both implement input data contract 1.12.0.
- Each GitHub Release carries
backcross-<version>-site.zip, the built site with a relative base that serves from any static folder (with a one-line server instruction, since Chrome and Edge do not run module scripts fromfile://), the CLI bundle, LICENSE and CITATION.cff; and the CLI bundle on its own. The release body is that version's CHANGELOG section plus the contract version it implements. - A user guide for breeders (docs/user-guide.md) with a validation-status section, and a docs index (docs/README.md).
CITATION.cff(validated in CI with cffconvert), a README citation section, and a sibling-tool section cross-linking progeny-selector.- Every analysis CSV (summary, QC, segments, targets, pairwise, discordant markers) gains three trailing columns after
crop:tool(backcross),tool_versionandtool_commit(g+ seven hex,-dirtywhen built from an uncommitted tree,NAwhen git was unavailable), and the HTML report a Software row and ageneratormeta tag, so a result names the release that produced it (docs/adr/0030).brapi-callsets.csvis unchanged. - A single-file CLI bundle,
dist-cli/backcross-cli.mjs, built bynpm run build:cliwith esbuild and attached to every GitHub Release; it needs only Node 22.19 and takes the same subcommands and options asnode src/cli.ts(docs/adr/0030). - A code of conduct (Contributor Covenant 2.1), a security policy, issue and pull-request templates and Dependabot configuration.
qc.csv: one row per sample in manifest order, parents included, with missing, heterozygosity and nonparental rates and|-joinedqc_flags, from the new CLIqcsubcommand and an Export screen button (docs/adr/0029).- The per-line summary CSV gains
max_marker_coverage_bpandmax_marker_coverage_cm, the resolved RPP maximum marker coverage the weighted estimators were computed with, beforetoken_profile(docs/adr/0028). ADR 0006 gains an amendment correcting its Flapjack comparability claims: interior weighting follows Flapjack, totals differ at chromosome ends by design. - Contract 1.12.0 (docs/adr/0027): the soybean scheme also reads the SoyBase / LIS Data Store names of Williams 82 (
glyma.Wm82.gnmN.Gm01,glyma.Wm82.gnm5.Chr01) and the V1.1 spellingGLYMAchr_01asGm01..Gm20; every spelling read before maps as before, and RefSeq accessions, other cultivars and Data Store scaffolds are still kept as written. - Contract 1.11.0 (docs/adr/0026): every text input (the genotype file plain, gzip or bgzip, samples.csv, markers.csv, and a custom token profile) must be UTF-8. A byte sequence that is not valid UTF-8 (a Latin-1 or Windows-1252
ésaved as 0xE9, an overlong encoding, an encoded surrogate, a sequence cut short at the end of the file) is now the errortext.invalid_utf8, naming the file, the line and the byte position (samples.csv line 4: not valid UTF-8 (byte 0xE9 at position 24)); before, backcross replaced it with U+FFFD without a word, so asample_idcould silently fail to match. Save such files as "CSV UTF-8". A byte-order mark is still ignored, and non-ASCII ids that are valid UTF-8 match across files. - Contract 1.10.0 (docs/adr/0025): the VCF GT grammar is stated. A sign, an exponent, an underscore, a space, a leading zero, an empty side (
/0,0/), three or more alleles, the VCF 4.4 leading phase indicator (|0|1), or an allele index above 127 or outside REF,ALT is now the errorgenotypes.invalid_gt, naming the line and the value; before,-5/0was stored as allele 251,1e1/0read as 10 and/0as REF. Haploid calls,./.,.,./1, an empty GT (missing) and multiallelic indices are read as before. - CLI subcommands
compareanddiscordantrun one pairwise comparison (algorithm 5) between--aand--band write the pairwise summary CSV and the discordant-marker CSV respectively;--modeselectsinformative(default) orall. A CI jobr-readergenerates every CLI CSV table (six sinceqc) from the synthetic fixture and reads them back withreadrunder explicit column types (scripts/read_exports.R), modelled on progeny-selector's r-reader job. - A crop scheme for sunflower (contract 1.9.0, docs/adr/0024). The Crop select and
--cropnow offersunflower, whose canonical names are bare1..17:Ha412HOChr01(HA412-HOv2.0) andHanXRQChr01(HanXRQr2.0-SUNRISE) are both read as1, because the two assemblies share the LG1-LG17 numbering (XRQ and HA412-HO v1 by their map anchoring, HA412-HOv2 by a whole-genome alignment against XRQ; docs/adr/0024 gives the evidence and its weakest link), andchr1,chromosome-1,01and1are read too. Unplaced scaffolds (HanXRQChr00c001), organelles, accessions (NC_035433.2,CM007890.2), theLGprefix and HA412-HO v1.1 names (Ha1) are kept as written. A case undercontract/cases/pins the spellings, including two it leaves as written. - The shared input contract is version 1.8.0: a genotype cell pairing two of
N,-,., in any order and in all three spellings (N/N,N-,./N,..,-|N), is read as missing in HapMap and in nucleotide-mode wide CSV. Both tools already did this and neither changes; the contract now says so, with four cases undercontract/cases/and a mirror in progeny-selector (docs/adr/0023). A pair withXthat is not itself a missing token (X/X,XN) stays an error, and under abase: nonetoken profile (dart,axiom,kasp) such a cell isgenotypes.unknown_cellunless the profile lists it, asaxiomlists--. A wide CSV holding such a cell that is not itself a missing token is read as nucleotide byautodetection. - The demo link takes a crop:
?demo=synthetic&crop=pealoads the demo under that crop scheme and leaves the Crop select on it, and "Copy link to this demo" adds&crop=<id>for any crop but soybean, so the plain?demo=syntheticlink is unchanged. An unknown crop in the address shows an alert naming the built-in crops and loads nothing. The Crop select also starts at the loaded dataset's crop when you return to Upload (docs/adr/0017, amendment of 2026-09-26). - Crop schemes for cowpea, pea and peanut (contract 1.7.0, docs/adr/0022). The Crop select and
--cropnow offercowpea(Vu01..Vu11, which also reads NCBI'sVu01(old4)names in their eleven exact pairings),pea(chr1LG6..chr7LG7, which readschr4and4LG4but keeps a bare4as written, because pea tables use bare digits for two numberings that disagree) andpeanut(Arahy.01..Arahy.20, whereA01..A10,B01..B10and theAradu./Araip.names are kept as written). Sunflower stayed deferred then and followed as contract 1.9.0 above. Three cases undercontract/cases/pin each scheme's spellings, including one it leaves as written. - The shared input contract is version 1.6.0: a genotype cell pairing one of A, C, G, T with one of
N,-,., in either order and in all three spellings (AN,-A,A/N,.|G), is read as missing in HapMap and in nucleotide-mode wide CSV. Both tools already did this and neither changes; the contract now says so, with four cases undercontract/cases/and a mirror in progeny-selector (docs/adr/0021).AXandA?stay errors, and under abase: nonetoken profile (dart,axiom,kasp) the pair is stillgenotypes.unknown_cell. A pair of two missing characters (N/N,..) was left undefined by 1.6.0 and is defined by 1.8.0 above. - Crop selector (contract 1.5.0, docs/adr/0020): soybean (default), maize, rice, sorghum, wheat, barley, oat, common bean and cotton chromosome schemes; a chosen crop normalises and orders chromosome names by that crop's convention, so a maize
chr1is no longer displayed asGm01. Every CSV export gains a trailingcropcolumn and the report a Crop row. - Token profiles (contract 1.4.0, docs/adr/0019): the Upload screen, the CLI (
--profile) and the sibling read HapMap and wide-CSV cells under a named vocabulary,tassel,soybase-report(H heterozygous, U missing),dart(0/1/2/-),axiom(AA/AB/BB, NoCall, or 0/1/2) orkasp(X:X/X:Y/?), or a JSON file of the same shape. Every CSV export gains a trailingtoken_profilecolumn and the report a Token profile row; readers by position keep their columns. A profile with a VCF, a BrAPI source or a wide CSV that is coded A/B/H is an error, as is anHat a marker with an indel allele; a profile file is always recorded ascustom:<id>. The CLI names the file in a profile read error. Nine new cases undercontract/cases/. - A demo dataset. The Upload screen's "Try the demo dataset" button loads the synthetic test fixture (six generated lines, 500 markers) that the site serves under
demo/synthetic/, through the same load path as picked files. "Copy link to this demo" copies a link that opens the page at?demo=synthetic, which loads the demo on arrival and opens the summary; an unknowndemovalue is reported in the alert. The demo data are synthetic, never real genotypes (docs/adr/0017). - Accessibility review (M3): focus order verified in Chromium and Firefox from the skip link, through the rail, into every screen; a Lines row reached by keyboard is never hidden under the sticky header (WCAG 2.4.11); every status carries a text or ARIA cue beside its colour or weight; axe-core reports no WCAG 2.0/2.1 A or AA violation on any of the six screens, including the BrAPI form, a load in flight with its Cancel button, its error, the empty-filter table, the chromosome overview, a region error and the sort list open; and Lighthouse accessibility is above 90 on the built landing page, run by
npm run a11y:lighthouselocally and in CI. Visible changes: the "input coding reference" link says it opens in a new tab; the genotype canvases are exposed as images with their descriptions; the rail's steps are a toolbar inside the navigation landmark; cancelling a BrAPI load moves focus to the Upload heading instead of dropping it; and printing hides the rail, the action bars, the genotype toolbar and the hover panel, unrolls the Lines table, and keeps the legend swatches' colours and textures. - Load a variant set from a BrAPI v2.1 server (
/callsets,/variants,/allelematrix) from the Upload screen, fetched inside the worker so genotypes still never reach the main thread. Roles still come from samples.csv. Itssample_idis the call set's name, or itscallSetDbIdwhen names collide, with a warning. A call-set table download (brapi-callsets.csv), available before loading, gives the ids to build samples.csv from. Positions arestart + 1, or come from markers.csv when the server gives none. An optional bearer token is sent only in the request header. Each request times out after 60 s, and a load can be cancelled (docs/adr/0015). - Large bgzipped VCFs load with memory under twice their inflated size; the file is read as a stream inside the worker. The bound is measured after the load in Chromium and accounted for the genotype builder in both browsers; the decompressor's transient buffers are not measured.
- Corrupted or truncated gzip and bgzip genotype files are now rejected with an error naming the damaged member's byte offset, instead of loading wrong or missing genotypes: every member's CRC32 and length are checked, and a bgzip file must end with its end-of-file block.
- A left rail of the six steps in place of the row of screen buttons. Each step says whether it is the one showing, already done, or still blocked for want of a dataset; blocked steps are announced as disabled and are stepped over by the arrow keys, which now run down the rail rather than across a row. The rail collapses to a numbered strip -- on its own on the graphical genotype screen, where horizontal room is the scarce resource, and by a control that pins it either way for the rest of the session. The heading and the reminder that files never leave the browser tab move into it, and "Skip to main content" is still the first thing a keyboard reaches.
- The graphical genotype canvas surrounded by its context. The line names have moved out of the canvas into a gutter beside it, which stays put while the canvas scrolls sideways and toggles that line's place in the shared selection when clicked; a chromosome strip above the canvas names each track, and on a single chromosome gives the shown window's start and end in Mb; and below the canvas, on a single chromosome, a whole-chromosome overview marks where the window falls and recentres it when clicked. Figures in the exported HTML report are unchanged and keep their labels drawn inside the image.
- Every screen now takes its colours, spacing and type from the design tokens rather than from values written into the component. The app's own stylesheets (
base.css,shell.css,screens.css) name tokens and nothing else, and the canvas renderer is handed its font and colours read from the stylesheet instead of holding its own. Visible consequences: an error is now a bar in the one reserved alert colour (docs/adr/0009, amended 2026-09-11) rather than a bare paragraph, a QC line flagged as closer to the donor is shaded and has its flags set in a heavier weight instead of being tinted pink, and the call-rate histogram's bars are a neutral chart colour rather than the recurrent-parent class colour, which they were never reporting. The lint gate that enforces this now covers every component file, with the legacy allowlist deleted. - The Lines table rebuilt on React Aria's
Table, which is arole="grid"widget rather than a static table: sorting from a column header, multi-row selection with a header checkbox, and a header that stays put while the rows scroll, with enough scroll padding that a row reached by keyboard never lands underneath it. A density control (Compact, Default, Comfortable) in the Lines action bar sets the table's row height and the height of the action bar's own controls; it is session state and is deliberately not remembered across reloads. The selection rule is unchanged -- the table speaks only for the rows the filter leaves visible, and a selected line hidden by the filter stays selected. - A texture per genotype class on the graphical genotype canvas and its legend, implementing the minority-class hatch ADR 0007 recorded as outstanding: donor, heterozygous, nonparental and missing calls each carry a distinct diagonal, crosshatch or dot pattern in addition to their Okabe-Ito colour, and a bin whose majority call hides a different call underneath now shows that call's texture overlaid on the majority fill. Recurrent-parent and uninformative stay plain fills (ADR 0009, amended 2026-09-11). The overlay never appears on a column that only holds an interval-fill extension between two markers, so it asserts nothing about the genome between markers.
classSwatchCss(src/core/palette.ts) draws the same encoding as a CSSbackgroundvalue in the app legend and the self-contained HTML report. - One selection model behind the Lines table and the graphical genotype view: a shared sort, a filter (free text over sample, line, generation, family and QC flags, plus flagged-only and selected-only) and one row model, so sorting or filtering from either screen reorders the other. An action bar on both screens reads
Lines: n, visible: n, selected: n; "Select all" and "Select none" act on the visible rows and leave hidden selected lines alone. Reordering is done on the main thread, so a sort issues no worker request. - Donor segment calling with breakpoint bounds (
callSegments), target-region status and linkage-drag estimates (checkTargets,parseTargetSpec), and per-line, per-marker and dataset QC flags (computeQc); segments CSV and target check CSV exports; fixture expectations for segments under both gap criteria. - Upload, Summary and QC, Lines and Graphical genotype screens over a Web Worker that keeps the genotype matrix off the main thread: load with a parameter panel, per-line QC table and call-rate histogram, sortable line table with target-region status, and a canvas renderer with per-pixel majority binning (docs/adr/0007).
- Genotype-view zoom and hover, reading a marker's position, id, class and sample at any zoom level; a Compare screen for pairwise line comparison; an Export screen producing the HTML report and all CSV exports; keyboard navigation across screens; and parameter editing after load without re-parsing the genotype file (M2 milestone, PLAN.md).
- Pairwise line comparison in both modes, with per-chromosome counts, discordant markers in genome order and identity by state (the SNPRelate definition) in mode
all; the two pairwise CSVs of docs/data-formats.md. - A self-contained HTML report: dataset summary, warnings, the parameters every number was computed with, QC, lines, donor segments, target checks and a graphical genotype image for each line rendered into the report, with a print stylesheet for PDF.
- Parameters
maxSegmentGapBp(10 Mb) andmaxSegmentGapCm(10 cM) for donor-run breaking, distinct from the RPP coverage cap, with env varsVITE_DEFAULT_MAX_SEGMENT_GAP_BPandVITE_DEFAULT_MAX_SEGMENT_GAP_CM(docs/adr/0008). - Data-contract parsers for VCF 4.2 (plain and gzip/bgzip), HapMap, and wide CSV (nucleotide and coded A/B/H), with samples.csv and markers.csv validation at the boundary.
- Parent-of-origin classification per marker per candidate (RP-homozygous, donor-homozygous, heterozygous, missing, nonparental, uninformative).
- Recurrent parent proportion per line and per chromosome: marker-count, bp-weighted, and cM-weighted estimators.
- Per-line summary CSV export and a Node CLI (
node src/cli.ts summarize). - Synthetic 500-marker fixture with planted donor segments and analytically derived expectations; Vitest smoke and contract tests.
- Module stubs with fixed interfaces for donor segment calling, target-locus check, pairwise comparison, QC, canvas renderer, worker protocol, and the six UI screens.
- GitHub Actions workflow (lint, typecheck, test, build, fixture reproducibility, GitHub Pages deploy).
Changed
- The RPP coverage cap is named maximum marker coverage everywhere:
maxMarkerCoverageBp/Cm,--max-marker-coverage-bp/-cm,VITE_DEFAULT_MAX_MARKER_COVERAGE_BP/_CM, and the report rows;max_gapcollided with the donor-run gap of docs/adr/0008 (docs/adr/0030). rpp_cmcredits both terminal informative markers half the maximum marker coverage on their outer side, since a linkage map's 0 cM is its first marker rather than the telomere;rpp_cmtherefore changes for a line whose first called informative marker on a chromosome lies within c/2 of 0 cM.rpp_bpis unchanged (docs/adr/0028, amended 2026-09-29).- Data contract 1.3.0 (docs/adr/0018): blank and whitespace-only lines and rows whose every field is empty are skipped in every input, including before the header;
#no longer starts a comment (a#row in the wide CSV, samples.csv or markers.csv is data, and a HapMap must begin with itsrs#header), so a file that relied on#comments now fails naming the line; a quoted field may contain a line break, as Excel writes them, and is read with LF line breaks; a"opens a quoted field only at the start of a field and is otherwise an ordinary character; a quoted field left open at the end of a file, and a#line after a VCF#CHROMline, are errors naming the line. Sixteen new cases. - The line gutter's buttons beside the graphical genotype canvas are 24 px tall (WCAG 2.2 SC 2.5.8); the canvas rows are 20 px with a 4 px gap, up from 14 and 4. The accessibility tests now include the WCAG 2.2 target-size rule.
- The tool is renamed from Isoline Browser to Backcross (docs/adr/0017). The page title, rail heading, HTML report title and footer, package name and repository links use the new name, and the GitHub Pages site moves to https://piercetaylor.github.io/backcross/. Export file names begin
backcross-(backcross-summary.csv,backcross-segments.csv,backcross-targets.csv,backcross-pairwise.csv,backcross-discordant-markers.csvandbackcross-report.html); they beganisoline-before 2026-09-22 (docs/adr/0017, amended). A script that picks up a downloaded file by name needs the new name. - The interface chrome uses a crop colour palette: parchment page and rail surfaces, soil-brown text, leaf-green links, primary button, current-step marker and focus ring, and a wheat-gold edge on the Upload screen's intro band. The rail heading has a leaf-and-seed mark, and the site has an SVG favicon. The genotype class colours, textures and everything drawn in the genotype view are unchanged (docs/adr/0009, amended 2026-09-16).
- Data contract 1.2.1: wording only. The contract names the canonical repository
backcross; no input is read differently. - The build now emits React and ReactDOM, the other dependencies, and this application's own code as three separate scripts, plus Rolldown's small shared runtime chunk.
npm run buildfails if any script exceeds 500 kB, the application script exceeds 256 KiB, or the runtime chunk exceeds 16 KiB. - The graphical genotype view draws only the rows in view: the canvas and the line gutter scroll inside a bounded container and lay out the visible rows plus four either side, so a whole-genome draw costs the visible rows rather than every line. The overview below a single chromosome now represents every line by binning lines into its pixel rows with the same majority rule the columns use, instead of drawing an evenly spaced subsample. Printing still draws every row.
- Every CSV export and the HTML report now carry the BrAPI
call_set_db_idandsample_db_idof each sample, immediately after the sample id column(s) (_a/_bin the pairwise tables). The cells are empty for a dataset loaded from a file, and the report shows the columns only for a BrAPI dataset. Readers that pick columns by position must be updated; docs/data-formats.md lists every header. - Data contract 1.2.0 (docs/adr/0014, amended 2026-09-14): a position written as a whole-valued float or in exponent notation (
1000.0,1e3,1.9E+07) is read as that integer in HapMap, wide CSV and markers.csv; a position with a fraction (100.7), an empty position or a negative one is now an error naming the line and value, where100.7was kept as a fractional position and an emptypos_bpread as 0; an all-empty wide-CSV row (,,,) is skipped instead of becoming a marker with an empty id at position 0, and an emptymarker_idin any other row is an error. VCF POS must be decimal digits; a VCF record with ID.is named from the parsed POS. Six new cases undercontract/cases/. - Data contract 1.1.0 (docs/adr/0014): the chromosome pattern now states the
ChromosomeandLGprefixes and the_, space and-separators (soybean-only in this version); non-soybean names order naturally; one nucleotide vocabulary for HapMap and wide CSV (src/io/calls.ts): IUPAC codes R Y S W K M are read as the heterozygote in wide CSV too, the missing tokens are listed per format and mode (.|.added;X,XXin HapMap only; coded mode accepts only empty,NandNA, so-,--,.,./.,.|.andNNin an A/B/H file are now errors naming the cell), and every other cell (?, a strayBorH,0,+,A?,X/XXin wide CSV) is now an error naming the line and cell instead of an allele symbol; A/B/H auto-detection reads the whole file instead of its first 2000 rows; CRLF line ends and a leading byte-order mark are documented;line_nameis documented optional; samples load in manifest order with absent genotype columns dropped; a VCF record with ID.is named from CHROM as written; the genotype-file paragraph and the wide-CSV column order state the shared minimum; calls are diploid only. Fifteen new cases undercontract/cases/. New docs/input-coding.md, linked from the README and the Upload screen. - The input file contract (chromosome names, VCF, HapMap and wide CSV genotype files, samples.csv, markers.csv and class codes) has moved from
docs/data-formats.mdtocontract/data-contract.md, version 1.0.0, shared byte for byte with progeny-selector and checked by example cases undercontract/cases/. The wording is unchanged and no input is read differently;docs/data-formats.mdkeeps the analysis parameters and outputs. - The graphical genotype view draws the visible rows in display order, and the HTML report's figures inherit that order. This replaces the rule that it drew the selection, or every candidate when nothing was selected; the selected-only filter is the equivalent of the old behaviour (docs/adr/0009, amended 2026-09-11). The CSV exports are unchanged: they are computed over the whole dataset by contract.
- Line table columns sort naturally on embedded numbers, so
NIL_2now sorts beforeNIL_10rather than after it. Numeric columns place NA last whichever way the column points, and ties keep the worker's candidate order. The table gains a sortablefamily_idcolumn, and the QC flags column is now sortable. - The canvas renderer extends each marker across the pixel columns nearer to it than to any other marker when markers are sparser than pixels, which is what zooming into a region produces, so a track reads as contiguous blocks instead of one hairline per marker (docs/adr/0007). This changes how every existing view is drawn.
- The donor-run gap is measured between consecutive informative markers (PLINK
--homozyg-gapconvention) rather than between consecutive non-RP calls, somaxMissingSpanis the only control on missing calls; the segments CSV gains a trailinggap_criterioncolumn. - docs/design-brief.md and docs/adr/0009: the interface design language for milestone M2.5, with the prior art it follows and the contrast measurements behind the texture encoding.
- The CLI rejects an option that belongs to another subcommand (for example
--max-marker-coverage-bponsegments) with exit code 2 instead of ignoring it. - The README is a current overview with a reproducible synthetic example and links to the detailed records; the previous README is preserved in
docs/legacy-readme.md.
Fixed
- A target on a chromosome the loaded dataset lacks, such as
chr13under pea when the names areGm01..Gm20, is now reported: the Lines screen shows a warning under the targets andnode src/cli.ts targetsprints one to stderr, each naming the chromosome and the first few loaded names. Its column and CSV rows still reportno_dataand the CLI still exits 0, so one target list can still be run over several datasets. It was a silentno_databefore. - The Graphical genotypes Region field reports a chromosome the loaded dataset lacks, such as
chr13under pea when the names areGm01..Gm20, and leaves the view where it was. It went to an empty canvas with no message before, while the View select still read "Whole genome". - "Try the demo dataset" loads under the crop chosen in the Crop select. It always loaded as soybean before, whatever the select showed.
- The genotype view's browser geometry tests wait for the rail-collapse animation before reading an absolute x. Entering that screen collapses the rail and the shell animates its grid track, so a measurement taken while the track was still moving was a transient, and the gutter case failed intermittently in CI against a settled scroller.
Implements input data contract 1.12.0.