Changelog¶
All notable changes to this project are documented in this file. The format follows Keep a Changelog, and the project adheres to Semantic Versioning.
[Unreleased]¶
Added¶
- An integrated evolutionary-genomics and surveillance vocabulary. Rectangular
TreeTrackbranches can now be orthogonal, diagonal or curved without changing topology;AncestralStateLayer,BranchEventLayerandBranchIntervalLayerkeep state posteriors, direct events and uncertainty on their correct nodes or edges in rectangular, circular and unrooted projections.PhylodynamicTrackadds linear or log trajectories with uncertainty ribbons, whileSurveillanceTrackadds observed lineage counts, denominators, stacked or line views, sampling floors and explicit frequency or growth alerts. Theevolutionary_surveillanceexample joins these with molecular selection, genomic layers and a core-versus-accessory tanglegram on one eight-panel sheet. - Molecular-selection figures can now preserve the structure of branch-site
and site-wise results rather than compressing every analysis into one branch
colour.
BranchRateMixturedraws weighted ω-class capsules on rectangular, circular and unrooted trees;HomoplasyLayerconnects equal direct branch events as recurrence candidates; andSelectionTrackaligns p-value or posterior evidence above a signed, neutral-centred site effect. Missing estimates remain missing, source weights and rates stay exact in SVG tooltips, and evidence is never allowed to impersonate effect size. Theselection_atlasexample combines all three with protein architecture and iTOL-style terminal metadata.
Changed¶
- Release builds now check arithmetic for overflow. A wrapped add is a figure
that is quietly wrong, which is the one thing this crate is not allowed to
be:
Genomeadded lengths without saturating, and a release build turned that into boundaries running backwards and every point drawn on the wrong sequence, with nothing said. A branch per arithmetic operation is nothing in a crate whose work is string formatting, and it converts the whole class from silent to loud. A hundred and fifty thousand generated figures and every example render clean under it. - Nine functions turned an
f64into text; there are now three, and the two that remain outsidesvgare four-line wrappers over them. Two of the copies were literally identical and a third was the same plus a check forNaNthat the others never received, which is what a copy costs: a fix reaches the copy the bug was found in and stops there. The same mistake had by then appeared three times in three modules.svg::text_roundedandsvg::text_exactare the two ways a number reaches a reader, and both carry the guards;svg::numis unchanged and remains the way a number reaches an attribute.svg::text_number, added earlier in this same unreleased version, is gone in favour oftext_rounded. svg::finite_withinreplaces the two private helpers, one in the tree track and one in the map, that differed only in whether the upper end was named. Almost every setting a drawing takes is a length, an angle or a fraction and none of them has a reading forNaN, so there is now one answer to what happens then. Four pixel thresholds that had no answer at all, inLogoTrack,PileupTrackandSequenceTrack, use it.- The three largest files became directory modules, split along what they
actually do rather than at a line count.
src/track/tree.rswas 4,884 lines, two and a half times the next largest, and is now seven files: the three projections apart (rectangular,radial,unrooted), the layout they all read (scene), what is drawn on top of any of them (decorate), the two ways a branch length is given a unit (scale), and the type and its vocabulary inmod.rs.src/map.rsbecame the plain map,phylo, the marks both compositions share (draw) and the projection maths (project), which is worth its own file because every function in it can answer that it will not place a point.src/tree.rsgave up its Newick, annotated Newick and NEXUS parsing totree::parse, which is where all the format's ambiguity lives. Nothing else moved: the remaining files are one subject each, and splitting a coherent module into thirds makes it harder to read, not easier. Every public path is unchanged, and all forty-one committed figures render byte for byte the same, which is what makes this a move rather than an edit. - The format readers moved from the command line binary into the library, as
karyon::read. Four thousand lines that parse BED, bedGraph, GFF3, VCF, SAM, cytoBand,samtools depth, FASTA and Newick were reachable only by running the command, so writing Rust against this crate meant writing a VCF parser first, which is the opposite of what a crate that ships twelve file-backed tracks should ask for. The rule that made them live there is unchanged and now says something sharper: every reader takes a&str, so nothing in the library opens a path, and the binary keeps the one part that is genuinely the command line's.Formatmoved with them, since it names file formats rather than flags.
Fixed¶
SquiggleTracknever finished drawing over a wide region. The envelope branch walked the axis positions a pixel column covers rather than the samples in it, so over a chromosome each column asked thousands of bases whether they happened to be a sample, and over the whole coordinate range it asked billions and the figure never came back. The walk is now cut down to where samples can be before it starts.Genomeadded lengths and gaps without saturating, in all seven places it accumulates. A debug build panicked; a release build wrapped, which is worse, becauseboundariesthen came back decreasing and every point in a genome-wide plot landed on the wrong sequence with nothing said.- A figure whose tracks each reported a height that was a number could still
report a total that was not. The height check reads one track at a time, so
two finite heights adding to infinity passed it, and the document then said
height="0"whileFigure::dimensionssaid infinity: the file and the API disagreed about the same figure. The total now has a ceiling at the largest integer an f64 holds exactly. - Tree scale bars, support labels and clade support tooltips printed a large
value as three hundred digits. They were using
num, which writes a coordinate and stays plain on purpose.svg::text_numberis the sibling of it for numbers a person reads, agreeing withnumto the byte below two to the fifty-third and switching to an exponent above it. This was the third place the same mistake appeared, afterVariantTrackandMap, which is why it now has a function of its own rather than a third patch. - A map tooltip printed
f64::MAXas three hundred and nine digits. The same bug as the one fixed inVariantTrack, in the other writer, found because the properties had never been pointed at a map:data_numberhanded the value straight to a formatter, which expands it in full. An f64 stops holding consecutive integers at two to the fifty-third, so past that all but the first sixteen digits are the formatter filling in a gap rather than anything measured, and the same happens under a thousandth as a run of zeros. Outside that range the value is now written as an exponent, which says the same number and says how much of it is known. - Six committed figures disagreed with the code that draws them: the MSA row
labels had moved to full ink, four phylogenetic figures had shifted
geometry, and the gallery had grown. A seventh, the node faces figure,
reached the documentation without its
assets/original ever being added. The staleness gate could not have caught that last one, becausegit diffcompares against the index and a file that was never added is not stale to it, it is invisible; the gate now stages the renders first.
Added¶
- A branch-wise dN/dS visual grammar for
TreeTrack.dndsuses a colour-vision-safe logarithmic scale centred on ω = 1 instead of stretching the observed minimum and maximum across a generic ramp. Values are direct branch annotations and never inherited; missing or invalid fits are dotted.dnds_significancecan use a p- or q-value to carry evidence through stroke weight while colour continues to carry effect size. Rectangular, circular, unrooted and cladogram examples share one explicit legend and exact SVG tooltips. - The generated figures now reach all thirty-three track types, not sixteen, and build the tree ones through their whole builder rather than their constructor. The fourteen original gaps had never been given a hostile input and eleven numeric setters had never been given a hostile number; four bugs came out of it, each one now pinned by a test that was watched failing first.
- A property over every reader at once: the same interval written as BED,
GFF3, cytoBand, SAM, bedGraph,
samtools depth, VCF, an association table and a genotype matrix has to come back as the same two 0-based numbers. The audit beside the readers already pins every format to one known base; this asks the same question without picking the base, because a reader can be right at position 100 and wrong at 0, and because two readers disagreeing is what an off-by-one becomes once a figure stacks them. Every one of the six places the crate converts from 1-based was shifted on purpose to confirm the property notices. It also checks that naming a format and letting the reader work it out give the same answer, which is where these readers went wrong in practice rather than in the arithmetic. - Twelve properties over phylogenies and maps, the two subsystems the suite had never covered, and each one checked by breaking the library on purpose to confirm it notices. Trees: every node placed exactly once with the tips filling the rows, a parent between its children, the rows identical whether depth is measured or counted, a cladogram depth equal to the number of branches, and a parser that decides rather than panics on a file that is not a tree. Rerooting is held to the only thing it must not do, by an oracle that walks the undirected edges itself: every tip-to-tip distance survives rooting on an internal node, on an outgroup and at the midpoint, and a refused rerooting leaves the tree untouched. Maps: a valid document, the same document twice, no location dropped without the notice saying how many, coordinates kept rather than clamped, and nothing projected off the page. The last of those covers path data as well as attributes, since that is where a projection actually lands.
- Row-major
Panelsgrids for balanced comparison sheets, plus quieter clade fields, halo-separated node compositions, compact annotation legend chips, aligned-tree dividers and washed domain architectures. - Node-attached bubbles, pies, donuts and stacked bars from exact numeric
annotations; clade highlights that become rectangular bands, radial sectors
or unrooted fields; tree-aligned MSA rows; and a new
DomainTrackfor domain, motif, exon or repeat architectures ordered by phylogenetic descent. - Crossing-aware tanglegram untangling, equal-angle unrooted trees and layered iTOL-style annotation strips, bars, binary markers and symbols.
- Projection-independent branch evidence for phylograms: support can use scaled symbols, exact labels and a threshold in either common convention; branch-specific annotations can follow their edge without ancestral inheritance; and automatic or exact scale bars retain evolutionary units in rectangular, circular and unrooted coordinates.
- Validated rerooting around an internal node, a strictly monophyletic outgroup or the weighted midpoint. Tip distances and edge support are preserved, invalid requests do not mutate the tree, and rooted projections can mark the selected root explicitly without leaking that marker into unrooted views.
- A visual plot catalogue for the documentation, separating discovery from the exhaustive track reference. Eight category pages group all thirty-three tracks and the three standalone drawings by biological question, data shape and coordinate system, with responsive image-led cards, shorter section-only tables of contents and direct routes to the exact API contract.
- Every figure now says what it is. The document carries a
<title>, a<desc>,role="img"andaria-labelledby, so a screen reader is given a sentence instead of several thousand unnamed rectangles, and a reader whose image did not load is given the alt text.Figure::descriptionsets that text; without it the description is composed only from what the figure knows for certain, the region and the labels of its tracks in order, which says what is there without claiming anything about what it shows. SvgWriter::begin_titledandSvgWriter::describe, and a tooltip on every glyph worth pointing at: a gene gives its name, span and strand, a variant its position, consequence and allele fraction, a read its span and CIGAR. A<title>as the first child of a group is SVG 1.1, so this needs no script, fetches nothing, and survives Inkscape and Illustrator.- What gets named follows the rule the crate already binned by: the thing that decides whether a datum is drawn on its own decides whether it is named. A coverage bin is the resolution of the output rather than of the input and gets nothing; a dense row-major track names the row and not the cell; a glyph under a pixel is nothing a pointer can rest on. The worst a figure grew was a third, and the genome-wide figure did not grow at all.
SvgWriter::begin_titled_inertfor decoration drawn over data. The ideogram's region marker is a translucent rectangle written after the bands, so its own tooltip took every hover inside the highlighted span and the bands it points at were the one part of the chromosome a reader could not interrogate.plotandPlot, a short way to write a figure down.Figuretakes tracks that already exist, which is right when a track is built elsewhere and wrong for the ordinary case: a handful of tracks written out once, in place. There the plumbing was most of the code. ARegionto unwrap, auseline naming every track type, apusharound each one, anAxisTrackto remember, and the window start repeated on every track that takes one even though the figure already knew it.plot("NC_000962.3:761,000-763,000")?starts a stack and oneadd_call per track fills it, in the order they stack.Plotholds the track added last in its type parameter, soPlot::labelnames it andPlot::adjusthands it to a closure that sees the concrete type. Every builder method on the track is therefore in reach, and a name that does not exist on it is a compile error rather than a call that goes nowhere. This is the reason the type has a parameter at all.- Two things a plot fills in, both of which can be undone. An
AxisTrackgoes on the bottom, since a figure without coordinates along it is rarely what anyone meant;Plot::add_axisputs it elsewhere andPlot::remove_axisleaves it out. Tracks carrying an array rather than coordinates start at the left edge of the region, with an_atform for the ones that do not. Plot::done, which puts the pending track away without adding another. A stack built in a loop or behind a condition needs every arm to have one type, and the type parameter that makesadjustwork is what takes that away.Plot::add_trackandPlot::add_boxed, for a track that is already built: one an alternative constructor made, one read back before it is drawn, or one from outside the crate. This is not a fallback but the ordinary way to write a heavily configured track, since the builder chain says it in one line andadjustwould wrap the same calls in a closure.- A command line front end,
karyon, in its own binary target. The library still reads no files and still has no dependencies: everything the command reads is line based text, and the reading lives insrc/bin/karyon/. The grammar is the facade with spaces instead of dots, becauseargvis already an ordered list whose later words describe the earlier ones: each--<track>flag starts a track and the flags after it describe that one, so the order of the flags is the order of the stack. - Twelve tracks have a standard text format to read and are the ones the
command has. BED, bedGraph, cytoBand, GFF3, VCF, SAM, FASTA, Newick and two
tables. BAM, CRAM and BCF are not read at all and are not meant to be: they
come in through a pipe, since
samtools depth,samtools viewandbcftools viewalready write what these readers take. - Coordinates are read as each format defines them, which is the one thing in
the command that would fail silently. BED, bedGraph and cytoBand are 0-based
half-open; GFF3, VCF, SAM and
samtools depthare 1-based. Every reader has a test that pins a known base through the conversion, and the end coordinate is pinned separately from the start. Tree,CladeandPlacementat the crate root.TreeTrack,CladeTrackandTanglegramTrackwere re-exported there and all three take aTree, which was not.- Typed phylogenetic annotations through
parse_annotated_newickandparse_nexus, including BEAST, NHX, rootedness markers and Nexus translation tables.Treenow exposes MRCA, ancestor and descendant queries, rotation, ladderising, internal-node rerooting, subtree extraction and collapse. - Dated
TreeTracklayouts, metadata-driven branch colours, visible internal nodes, non-destructive clade triangles and categorical or continuous trait columns aligned to the terminal taxa. The synthetic surveillance example is a committed visual regression target for the complete composition. - Circular
TreeTrackprojections with complete or partial sweeps, controllable start angle and central gap, outward or inward branch direction, concentric time guides, annular categorical or continuous trait rings and non-destructive collapsed-clade wedges. The four-view radial gallery is a committed visual regression target. Map,GeoLocation,GeoFlowand three explicit geographic projections. Locations carry exact coordinates, counts and categories; categories use shape as well as colour, weighted links retain direction, and invalid, ambiguous or hidden inputs are counted rather than clamped or dropped. The Natural Earth 1:110m land geometry is public-domain data compiled into the crate, so rendering stays offline and deterministic.PhyloMapandPhyloConnector, composing an inward circular time tree, phylogram or cladogram around a central map. Terminal annotations match a unique supplied coordinate table, with aggregated, individual or absent connectors; the renderer does not infer coordinates, ancestral states or migration. A two-view circular phylogeography gallery pins the composition as a visual regression target.From<Error> for std::io::Error. A program that draws figures spends its errors on writing files, so its functions returnio::Result, and the one call that parses a region was the only thing in such a function that could not use?. That is a poor reason to reach forunwrap, and it was the second line of all fourteen chains in the examples.Empty,SlotandNamed, the plumbing that givesPlotits type parameter, underkaryon::plotrather than at the crate root.Emptyis deliberately not aTrack, solabelandadjustbefore any track has been added are compile errors rather than calls that go nowhere, and a figure cannot be asked to draw the empty slot.SlotandNamedare sealed.CodonTrack, a ruler in codons. A variant in a coding sequence is named by residue (BRAF V600E, TP53 R175H, rpoB S450L) and the crate could only speak bases, so no figure could be pointed at with the name its result has. It partitions a coding sequence into codons, numbers them, and translates them where a letter fits. The partition is itself the claim: two changes in one codon are competing alleles at one residue, not a double mutant. On the reverse strand codon 1 sits at the highest coordinate, which is the whole reason this is a track and not a division by three: roughly half the coding sequences in any annotation run backwards, and getting their numbering wrong is silent.CodonTrack::genetic_code, since the translation table was hard-coded to NCBI table 1. Table 11 gives the same residues so bacteria, archaea and plastids were fine, but a mitochondrial or ciliate sequence was quietly translated into a plausible protein that was wrong.SplitReadTrack,SplitReadandSplitSegment, reads that align in pieces. APileupTrackread is one start, one CIGAR and one strand, so a molecule that visits three places cannot be written down in it, and aStructuralTrackarc starts from a finished call, so by the time there is an arc the evidence has been summarised away. This is the evidence: one row per molecule, one bar per alignment, connectors saying in what order and orientation it went. A backward hop dips under the row and a forward one arches over, so a read crossing an inversion is a different picture from one crossing a deletion. A transposition is three segments and not an arc.CladeTrackandCladeBlock, genomic intervals painted onto a phylogeny. AMatrixTrackcell is one base wide and cells never merge, so a matrix can only say that six samples each carry something here, which is six observations. A block whose height covers a clade says one. The track can lie in exactly one way and refuses to: rows inside a block that do not carry it are cut out, so a paraphyletic set can never pass for a clade.TranscriptionUnitTrack,TranscriptionUnitandTerminator. Nothing in the crate touched transcription. The span from the bent arrow to the hairpin is one RNA molecule, so the genes under it are co-transcribed and a promoter mutation upstream changes all of them at once. The 5' leader is drawn hollow, which makes its length a distance to measure rather than a number in a caption, and a leaderless transcript is a different picture rather than a different label.examples/reading.rsand four assets, plus gallery panels S to V. The sheet is twenty-two panels.- The README track table now lists all thirty-three track types. It had stopped
at
AxisTrack.
Added¶
tests/properties.rs, which asserts invariants rather than examples. A test written beside the code inherits the blind spot of whoever wrote it, because the person writing the assertion is the person who believed the code: a reverse-strand span was wrong here from the first commit and every test of it agreed with it. An invariant can be checked without knowing the right answer, which is the only way to find something nobody thought of. Seven of them so far: every generated figure renders a valid document, rendering is deterministic, the reported dimensions are the ones in the document, a locus string survives a round trip, the scale never goes backwards, changing the theme changes no geometry, and a track added below never shortens the figure.- The figures are generated by a seeded LCG rather than by a property testing
crate, so the dependency count stays at zero even for the tests, and a
failure is reproducible from the seed in the message. What it feeds in is the
point: not plausible depths but
NaN, both infinities,f64::MAX,u64::MAX, empty vectors, one-base regions and names carrying markup. Ten thousand figures take about three seconds.
Fixed¶
- A variant tooltip printed
f64::MAXas three hundred and ten digits. The guard against that existed and its fallback expanded the number anyway, and its threshold was above the point where the precision it was protecting had already gone: anf64holds consecutive integers to 2^53 and therefore consecutive hundredths only to a hundredth of that, so 999,999,999,999,999 came back reading.04. Past that the magnitude is all there is to say, and a new invariant says no tooltip may carry more than twenty digits in a row, which is whatu64::MAXneeds and more than any grouped number reaches. - Six coordinate overflows, every one found by the property suite on its first
run and none reachable with ordinary data:
Scale::bounds,Read::endand the CIGAR walk, the codon walk inOrfTrack, and the sequence track's right edge. They are the same defect in six places. A coordinate near the top of the range does not survive a trip throughf64, sinceu64::MAX - 10is not representable, rounds to 2^64 and comes back asu64::MAX, so anything added to it overflows. All of them saturate now, which is what the arithmetic in the rest of the crate already did: a panic is what happens in a debug build and a wrapped coordinate is what happens in a release one, and the wrapped coordinate is the worse of the two because it draws.
Thirty-five defects, found by hunting the crate along seven failure modes and then handing each claim to an independent reviewer whose job was to refute it. Seven were refuted. Every one below has a reproduction that runs and a test that pins the answer.
Wrong figures.
- Reverse-frame open reading frames were a whole codon out at both ends. They
swallowed the stop codon that is supposed to bound them and dropped their
last codon, so
OrfTrackreported 94 codons where there were 93 and painted the bar over the very tick that ends it, while the three forward lanes beside it were right. Frame -k of a sequence walks the codons frame +k of its reverse complement walks, and over 5,520 random sequences 5,519 disagreed with their own mirror. They now all agree, and the property is a test. MatrixTrackput its cell half a floor-width to the right of the site it stands for, so it did not line up with the Manhattan point above it in the same figure.contrast_inkchose the less legible ink for adenine and guanine. The contrast ratios say so, and now the ink follows them.AxisRingplaced its labels from the outer radius alone, so a thin ruler printed its coordinates on top of the ring inside it.- Every track was clipped to the widest value axis in the figure, so a track that asked for no axis could paint into the strip another track had reserved.
- A track label longer than the gutter was drawn off the left edge of the canvas and lost its first characters; it is ellipsised now.
Panelssized the sheet from the panel widths alone, so a caption could be painted over the next column.Panels::share_outdid not return the most level partition among those tying on the tallest column.FeatureTrackpacked features that were off screen, so the band height and the row of every visible feature depended on data nobody could see.AxisTrack's tick loop overflowed at the top of the coordinate range: a panic in a debug build, and 503 wrapped-around tick labels in a release one.- A doubled quote inside a quoted Newick label started a new taxon, inventing tips that are not in the tree.
Genome::atdid not bound-check, so a coordinate past the end of one sequence was drawn inside the next one and counted as mapped. It now accepts exactly the positionsGenome::locateinverts, which is a narrowing: the end of a half-open interval isGenome::offset, notat.
Data dropped without a word.
CodonTrackdrew nothing at all when the coding sequence length was not a multiple of three, or when the trailing partial codon was in view.- A structural variant whose two breakpoints straddle the whole window was dropped, so the largest events were the ones that vanished.
Tree::parse_newicksilently discarded every unnamed leaf, so a legal Newick tree lost tips and changed topology.- A UTF-8 byte order mark made the first data row of every column reader disappear.
- Track heights were computed against
Theme::defaultwhile the rows were packed with the figure's own theme, so any other font size could cut a row off the bottom of a band. Rings::linkcollapsed the origin-spanning end of a chord to a hairline.--matrixwhose sites all fall outside the region was a silently empty lane and exit 0.
Output that was not valid SVG.
--colorwas pasted into an attribute unescaped. That is attribute injection, not a malformed file: a value carrying<put elements of the caller's choosing into the document. Metacharacters are refused now.svg::numwrote the literalinffor finite inputs abovef64::MAX / 1000, which was the one guard the writer had against a non-finite number reaching the page.escapelet XML-forbidden control characters through, so a name read from a user's file could produce a document no parser accepts.- A theme font size below 1 made tracks emit a negative
font-size. - A non-finite figure height was written as
height="0", and on a sheet it stacked every later panel on top of it.
Panics on data.
u64coordinate arithmetic overflowed across eight constructors.- A large finite
Figure::widthpanicked with a capacity overflow. - A named origin-spanning feature panicked
FeatureRingwith a subtraction overflow. - A number on the command line unwound instead of returning an error.
And the gate that should have caught some of this. The CI step that fails when the committed figures disagree with the code rendered ten of the sixteen examples, so six figures could drift unnoticed, and it never checked the copies the documentation site serves. It renders all sixteen now and checks both.
- A nested figure named itself, which left every panel of a sheet unnameable.
<title>resolves to the innermost element under the pointer, so the title a figure writes for itself shadowed the onePanelsputs on the panel over the panel's whole area: all twenty-two panel tooltips in the gallery existed in the file and none could be reached. A document rendered with an id prefix is being nested, so it no longer names itself;role="img"inside anotherrole="img"also hid its contents from a screen reader rather than describing them. SnpTrackhung its tooltip on the rotated column label rather than on the data, so the panel itself answered nothing, andshow_positions(false)took every tooltip in the track with it. The cell is what a pointer lands on, so the cell now carries the site, the reference base and what that sample has. Only a cell that differs is named: agreement is the state the panel exists to see past, the colour already says it, and naming it cost a third of the file.MatrixTrackcounted every site in the track while the drawing loop skipped the ones outside the region, so a zoomed figure described cells that were not on the page. Both numbers now count over the predicate the drawing uses.- Four tracks spelled the coordinate conversion themselves as
start + 1, which dropped the guard on a degenerate interval, so a zero-length span came out backwards: a clade block at 100 to 100 read101 to 100. There is one span formatter in the crate now and all of them call it.
Changed¶
- Every module doc is at one level. The crate had been written by many hands
and read like two crates: eighteen modules carried a single line of
//!, among themfigure.rsat 462 lines of code,theme.rs,codon.rsand all four command line readers, whilebisulfite.rscarried three named sections and the crate root carried eight. A module doc now opens with one summary line and an orienting paragraph, and takes named sections in twos or more, since exactly one heading is worse than none. Headings state a claim or ask a question, the way "The frames are not the strands" does, rather than saying Overview or Usage. The spread went from 1 to 106 lines to 17 to 105, with the middle half of the crate between 28 and 35. - A module doc states the decision and the item doc keeps the reasoning, which
is a rule the levelling pass broke before it held it: seven modules ended up
restating their own
///docs, and in two of them the same sentence stood in three places counting the documentation site. Those are back to one place each. - The documentation site is one shape too. Every page title is a noun phrase,
which matters because the title is the navigation label, and thirteen of the
fifteen pages now close with the
## Nextblock the other tools in the lab use. The two that do not are the home page, which has the card grid, and the changelog, which is an include. - The examples are no longer all one organism, and three of them were showing
an assay the organism does not have. The bisulfite panel drew a CpG island on
a genome whose methylome is 6mA and which has no 5mC at all; it is now the
human H19/IGF2 imprinting control region, which is where a per-molecule
bisulfite plot with two populations comes from. The per-strand methylation
panel claimed Dam and GATC in a genome with no
dam; it is now E. coli K-12 at oriC, with the eleven real GATC sites SeqA holds hemimethylated. The pangenome matrix drew an accessory genome with gained and lost islands in a species that has neither, and is now Klebsiella pneumoniae. The clade panel needed a block that was lost more than once, which a clonal organism does not supply, so it is SARS-CoV-2 lineage-defining deletions with the nsp6 SGF deletion as the recurrent one. - Coordinates in the remaining figures now match the annotation they name.
rpoBwas drawn 1,050 bases long instead of 3,519 with an invented neighbouring gene beside it; the RRDR box sat on codons 525 to 572 rather than 426 to 452, contradicting the frameCodonTrackpins in its own doctest;RD1was drawn at 8 kb, on top of gyrA and gyrB, rather than at 4.35 Mb;eccCa1was drawn on the reverse strand when its locus tag has nocsuffix. The split-read panel asserted an empty donor site and twice the depth over that same donor, which no single sample can show, and put the element in two orientations for one insertion event. - Documentation throughout now states the general case rather than the bacterial
or tuberculosis one. Twenty-three sentences across the API docs and the README
said what the library is for in terms of one clade: circular plotting existed
because "a bacterial chromosome has no ends", protein coordinates because of
what is "clinically interesting about a bacterial genome", split reads for
IS6110. The crate is organism-agnostic and the prose now reads that way. Example and doctest data are untouched, since a figure has to be of something and rpoB at H37Rv coordinates is as good as anything.
Removed¶
AccumulationTrack,AccumulationCurve,DistanceTrack,FrequencyTrackandFrequency, with their examples, assets and gallery panels. All three tracks failed the only entry test this crate has: none of theirdrawmethods ever readsctx.scale. Their x axis was a count of genomes, a count of genomes and a list of sample names, so what they actually drew was a bar chart, a line chart with a quantile ribbon and a clustered heatmap. A general plotting library draws those better, andsrc/track/distance.rshad said so in its own header since the day it landed: "Nothing genomic. Both axes of this track are the sample list, so it does not use the figure's shared coordinate system."- Breaking, as the names were public re-exports. The analyses they carried are
worth keeping somewhere, but a rarefaction over a presence matrix is a
statistic, not a plot type, and it does not need a
Trackto compute it.
Fixed¶
MethylationTrack::hemimethylatedonly paired the two strands when both calls sat on the same coordinate, which is never true of a palindrome: the two 6mA of aGATCare one base apart, and so are the two 5mC of aCpG. The API forced the caller to lie about one of the two positions, and the E. coli oriC panel found it the moment its coordinates became real. It now pairs the nearest partner withinMethylationTrack::pair_within, one base by default.Panels::columnsnow cuts the panels where the tallest column comes out shortest, and levels the rest, instead of filling each column until it has passed its share. The old rule compounded: one column overshooting left every column after it short, and taking three panels off the gallery sheet turned a 159 pixel difference between the columns into 317. The cuts are now chosen by weighing all of them, which for a sheet of eighteen panels is nothing, and the same sheet comes out 66 pixels shorter with a 111 pixel difference.
0.14.0 - 2026-07-31¶
Added¶
Panels, several figures in one document, which is what a multi-panel paper figure is and what an overview of a whole library needs. Each figure is embedded as a nested<svg>inside a translated group, so nothing is reparsed and nothing is rewritten: a panel on a sheet is the same picture as the panel on its own, and there is a test that asserts exactly that.assets/gallery.svgandexamples/gallery.rs, one panel per kind of plot. When a new track type is added it gets a panel here, because an overview that quietly stops covering everything is worse than no overview: it looks complete.MatrixTrack::tree, the same phylogeny orderingSnpTrackalready had. For a pangenome it is the difference between a figure and a shrug: sorted by sample name an accessory region is a speckle, sorted by descent it is a rectangle, and a rectangle is a claim about the biology.assets/example-pangenome.svg, a presence and absence matrix ordered by its tree.track::tree::leaf_order, the row permutation both tracks now share, so they cannot drift apart on how an unnamed row is handled.
Fixed¶
MsaTrack::consensusbroke ties by the last residue seen rather than the first, contradicting its own documentation. It usedmax_by_key, which returns the last maximum. Two residues tied in a column therefore picked the wrong one, though the result was at least stable between runs.- Six clippy lints that had been failing CI: three functions over the argument
limit, two byte-string literals and a single-character
push_str.
Changed¶
- The CI workflow runs on
workflow_dispatchonly. Pushing no longer spends Actions minutes; run it from the Actions tab or withgh workflow run ci.yml --repo PathoGenOmics-Lab/karyon --ref main.
0.13.0 - 2026-07-31¶
Phylogenies, and the reason to have them here rather than in a tree viewer.
Added¶
karyon::tree, withTree::parse_newickreading nested clades, branch lengths, quoted names, scientific notation and internal labels, taking an internal label as a support value when it parses as a number and as a name when it does not. Nodes live in one flat list and refer to each other by index, so a ten thousand deep ladder lays out without touching the stack.Tree::layout, placing every node at a depth and a row, as a phylogram from the branch lengths or as a cladogram counting branches instead.TreeTrack, a phylogeny as a track of its own, withTreeShapechoosing between the two.SnpTrack::tree, which is the pairing the tree is here for. It draws the phylogeny in the strip beside the panel and sorts the rows by descent, so a clade's shared substitutions line up into a block instead of scattering down the panel in sample-name order. Rows match leaves by name, and a sample the tree does not mention keeps its place at the bottom rather than vanishing: a row silently dropped from a figure is worse than a row out of order.Error::InvalidNewick, so a malformed tree is rejected with a reason rather than guessed at.
Notes¶
- Branches are drawn rectangular rather than as diagonals. A diagonal would imply the tree says something about the space between two rows, and it does not.
0.12.0 - 2026-07-31¶
Added¶
SnpTrackandSnpSite, a panel of the columns that vary and nothing else. An alignment of closely related genomes is almost entirely agreement: thirty kilobases carrying thirty-four differences would spend 99.9% of a plot on the part that says nothing.SnpTrack::from_alignmentfinds the variable columns against a reference row and drops the rest.
The idea is the one snipit is built around. The implementation and the drawing are this crate's own.
SvgWriter::text_rotated, since a five digit position has to stand on end to fit under a column narrower than it is.
Notes¶
- The x axis of a
SnpTrackis not linear in the genome. Two adjacent columns may be nine bases apart or nine kilobases apart, and the spacing says nothing about which. Every column therefore carries its own position, and anAxisTrackdoes not belong underneath one: a ruler there would be a lie. - A cell matching the reference is drawn as a quiet bar rather than a letter, on the same reasoning as the alignment track: the matches are the noise.
0.11.0 - 2026-07-31¶
Added¶
MsaTrackandMsaSequence, a multiple sequence alignment drawn row by row. The coordinates are alignment columns rather than genomic positions, which are two different things, so the figure's region is the column space and the ruler counts columns. Ungapping a row back to reference coordinates is a real operation with real decisions in it and is not done silently.MsaDisplay, defaulting toDifferences. In a real alignment most cells agree and the agreement is the noise, so rows are drawn as a quiet bar and only what disagrees with the comparison row is painted.MsaTrack::compare_tonames a reference row; without one the consensus is used.MsaTrack::consensus, whose ties break by which row came first so that the same alignment always gives the same answer.MsaColoringandResidueClass, six physicochemical classes for proteins, six being how many hues the validated palette has. Cysteine goes with the hydrophobics, histidine with the positives, tyrosine with the polars, and glycine and proline keep their own.MsaTrack::max_rows, with the sequences that do not fit counted and reported on the figure rather than dropped quietly.- Two more example figures, one nucleotide alignment under a conservation logo built from the same sequences, and one protein alignment by residue class.
Notes¶
- Neighbouring cells of the same colour are merged into one rectangle. Twelve rows of a hundred and twenty columns come out as a hundred and twelve rectangles rather than one thousand four hundred and forty, which is the difference between a figure and a file no viewer will open.
0.10.0 - 2026-07-31¶
The last of the comparison plots on the roadmap: one set of alignment blocks, drawn two ways.
Added¶
AlignmentBlock, one alignment between a stretch of the query and a stretch of the target. Both spans ascend and the strand is a flag, which is how PAF records it and saves a conversion on the way in.DotplotTrack, which gives the target the vertical axis. Forward blocks climb, reversed ones descend, and a translocation sits off the main diagonal, so a rearrangement has a shape rather than a description.SyntenyTrack, which gives the target a second bar and joins the two with ribbons. An inversion becomes a twist. Ribbons are translucent so two crossing ones read as two, and every block is also drawn solid on both bars, so a thin ribbon still shows exactly what it connects.- Both take the whole target sequence with
target_lengthor a window of it withtarget_range, and infer the span from the blocks when given neither. - Two more example figures: two chromosomes disagreeing in three places, and the inversion close up.
0.9.0 - 2026-07-31¶
Two more kinds of plot, chosen because they answer consecutive questions.
Added¶
ManhattanTrackandAssociation, association statistics along the sequence with a threshold line and hits coloured and ringed above it.Association::from_p_valueconverts a p-value to-log10(p)and floors a zero at the smallest positive double rather than letting it become infinity.genome_wide_thresholdis the conventional-log10(5e-8), documented as a Bonferroni correction for a million independent tests and therefore the wrong number for most organisms.MatrixTrack,MatrixRowandCellScale, a sample by site matrix: a genotype matrix out of a VCF, or a pangenome presence matrix once its genes have coordinates. Row names go in the axis strip, so they size themselves.CellScale::Sequentialfor quantities and presence,CellScale::Categoricalfor genotypes, where the numbers name things rather than measure them.assets/example-association.svg, a Manhattan tower with the gene under it and the isolates carrying the haplotype under that, all on one x axis.
Notes¶
- A sequential matrix ramp starts a step off the page rather than on it. Three things have to look different in a genotype matrix: a sample that does not carry the allele, a sample that was never typed, and empty page. A pure surface at the bottom of the ramp collapses the first two into the third.
MatrixTrackgives every cell a minimum width, since variant sites are points and would otherwise be sub-pixel slivers. A cell's width therefore says nothing about how much sequence it covers.
0.8.1 - 2026-07-31¶
Added¶
- A value axis on
VariantTrack, so an allele frequency can be read off the plot rather than guessed from the height of a stem.VariantTrack::show_scaleturns it off.
It appears only when it would mean something. VariantStyle::Tick draws every
mark at full height by design, so it gets no axis; and a track whose variants
carry no values has no ceiling to label, only a convention that a valueless
stem is drawn full height. Putting a "1" on that would be inventing a
measurement. A pinned max counts as a ceiling even with no values.
Frequencies keep their decimals, since a scale that rounded 0.64 to 1 would be
worse than no scale, while large values still shorten to 1.5k and 2M.
0.8.0 - 2026-07-31¶
Quantitative tracks get a real value axis. Until now a coverage track showed only its maximum, crammed into the top left corner on a translucent patch, and a reader could not get a number off the plot at any other height.
Added¶
Track::y_axis_width, which a track implements to ask for a strip between the labels and the plotting area, andDrawContext::axis, the rectangle it gets. The figure reserves the widest request across every track, so the plotting areas still line up and the shared x axis survives. A track that wants no axis returns zero, gets a zero-width strip, and cannot draw outside itself: the clip covers the band and the strip together.- A value axis on
CoverageTrack: the ceiling and zero, labelled outside the plot, with a hairline across it at the top of the scale. Two ticks rather than a ladder, because a coverage profile is read for its shape and its order of magnitude and six gridlines would be more ink than the thing they measure. - The same for
LogoTrack, whose top and bottom values move out of the band.
Changed¶
- Track labels sit to the left of the value axes, so a track with an axis and one without still line their names up.
0.7.0 - 2026-07-31¶
Added¶
IdeogramTrack, a chromosome drawn end to end with a marker showing which part of it the rest of the figure is showing. It is the one track that does not map its data through the sharedScale, because a track that only showed the region on display could not say where that region is.BandandStain, withStain::from_namereading thegieStaincolumn of the UCSCcytoBandtable so a row of that file converts without a lookup table of your own. An unrecognised stain becomes the palest shade rather than an invented dark one.IdeogramTrack::barefor a chromosome with no banding, which is what a bacterial genome wants, andIdeogramTrack::highlightfor marking a span other than the one on display.theme::mix, which blends two colours. The cytogenetic grey ladder is mixed from the theme's own ink and page, so a dark figure gets a dark ladder rather than a light one left uninverted.SvgWriter::rect_outline,SvgWriter::path_strokedandSvgWriter::begin_clip_path, the last so bands can be painted inside an arm's silhouette and keep its rounded end.- Two more example figures: a banded chromosome above its detail tracks, and the H37Rv chromosome as a bare outline with rpoB marked on it.
0.6.0 - 2026-07-31¶
A pass over how the figures look, with the colour half decided by measurement rather than taste.
Fixed¶
- The categorical palette had two slots, a brown and a grey, that were 1.8 apart under protanopia and 7.8 apart even with full colour vision. Any figure with seven or eight categories was drawing two of them the same. The palette is now six hues, every pair of which clears the separation floor.
- The dark palette failed on every count: half of it sat outside the lightness band a dark surface needs, four entries were desaturated enough to read as grey, and two were 10.4 apart. It has been replaced with steps chosen against the dark surface and validated there.
Added¶
BaseColors::colorblind_safe, four nucleotide colours whose closest pair is 11.0 apart under deuteranopia. The conventional IGV colours remain the default and are now namedBaseColors::conventional, with their measured weakness stated: adenine and guanine are 1.7 apart under protanopia, which is the transition pair.Theme::corner_radiusandTheme::insertion, the latter so a pileup's insertion marks stop being a hardcoded hex.SvgWriter::rect_roundedandSvgWriter::circle_ringed.assets/example-dark.svg, because a theme nobody has looked at is a theme nobody has checked.
Changed¶
- A coverage area is now a wash under a drawn line rather than a saturated block: the line carries the shape and the fill only says which side of it is under the curve. Bars stay solid, since there the bar is the mark.
- Coverage fits its axis with a little headroom, so the tallest point reads as a peak instead of something that ran out of band. A pinned maximum is still taken literally.
- Variant markers are large enough to see and carry a ring in the page colour, so two variants a base apart read as two variants rather than one blob.
- Features and reads have rounded ends.
- Lines are 2 pixels rather than 1.2.
0.5.0 - 2026-07-31¶
Added¶
PileupTrack, aligned reads stacked into rows the way a genome browser stacks them, with mismatches painted against the reference.CigarOpandRead, which model an alignment rather than an interval. Every SAM operation has somewhere to go and consumes what the specification says it consumes, so converting a BAM record is one arm per letter.M,=andXall becomeMatch: the track compares the sequences itself rather than trusting the operation.Read::segments, which walks the CIGAR and places every piece on the reference, andRead::base_at, which finds the read base at a reference position through whatever insertions and deletions lie in between.PileupTrack::max_rows, defaulting to forty, with the reads that do not fit counted and reported on the figure instead of dropped quietly.PileupTrack::layoutreturns that count.ReadColoring::Strandfor strand bias at a glance, andPileupTrack::fade_by_quality, because a read with mapping quality zero could have come from anywhere and should not be drawn as solidly as one that could not.- A fifth example figure,
assets/example-pileup.svg, whose depth profile is computed from the same reads the pileup draws.
0.4.0 - 2026-07-31¶
Empirical Bayes stabilisation, the other half of the Logolas paper. A logo can now tell four sequences from four thousand.
Added¶
karyon::dash, an implementation of Dirichlet adaptive shrinkage. Each column of counts is modelled as multinomial with a prior that is a mixture of Dirichlet distributions, all centred on a background and differing in concentration. The mixture weights are fitted by EM across every column at once, so a well sampled column overrules the prior while a thin one is pulled towards the background. From Dey, Xie and Stephens, BMC Bioinformatics 19:473 (2018).LogoTrack::stabilizeandLogoTrack::stabilize_withto apply it, andLogoTrack::sample_sizefor the case where the input is a probability matrix and carries no record of how many sequences it came from.LogoTrack::dash_fit, returning the fitted mixture weights and, throughDashFit::shrinkage, how far each column actually moved. A figure that quietly moved the numbers should be able to say by how much.karyon::dash::ln_gamma, since the standard library has none and this crate has no dependencies.- A fourth example figure,
assets/example-logo-stability.svg, showing one set of proportions at three sample sizes drawn raw and shrunk.
Changed¶
LogoTrack::smoothingis not applied to a stabilised track. The shrunk composition is already strictly positive, so the smoothing would be a second repair on top of a first.
0.3.0 - 2026-07-31¶
Sequence logos gain the rest of the scoring schemes Logolas offers, and the
score is separated from where the baseline sits.
Added¶
- Four more scores on top of the existing three:
LogoScore::KullbackLeibler(p log2(p/q), the symbol's contribution to the divergence),LogoScore::Difference(p - q),LogoScore::Ratio(p / q) andLogoScore::OddsRatio. Together withProbability,InformationContentandLogOddsthese cover theLogolasset. Centering, which decides where the baseline of a background-relative logo sits, as a quantile of the column rather than a fixed median.Quantile(0.5)is the default and matches thequantargument ofLogolas;Centering::Noneleaves the baseline at "exactly the background".LogoTrack::edlogo, shorthand for log odds centred on the median.LogoTrack::max_extent, pinning how far the tallest stack reaches so that two panels of a figure can be compared honestly.LogoScore::uses_backgroundandLogoScore::unit.- A third example figure,
assets/example-logo-scores.svg, showing the same four columns under all five background-relative scores.
Changed¶
LogoScalingis nowLogoScore, andLogoTrack::scalingisLogoTrack::score. The oldEnrichmentDepletionvariant was the pairing of one score with one centring, which is now spelledLogoTrack::edlogo()or.score(LogoScore::LogOdds).centering(Centering::median()).- Smoothing now applies to every score measured against a background rather than to the one that used to exist.
Fixed¶
- An information content logo labelled its axis
2 bitswhile drawing it to the tallest column on screen, so the annotation and the picture disagreed. The two absolute scores now get the fixed axis their convention asks for: probabilities to one, information content tolog2(K), the way WebLogo draws DNA from zero to two bits. Two figures of two motifs are comparable again. - A fitted axis now measures only the columns inside the region on display, instead of letting an off-screen column set the scale.
- The scale annotation reports the value at the bottom of a two sided logo as well as the one at the top.
0.2.0 - 2026-07-31¶
Added¶
LogoTrack, a sequence logo over consecutive positions, with three scalings:Probability,InformationContent(the classicseqLogoplot in bits) andEnrichmentDepletion, which scores each symbol aslog2(p / q)against a background, recentres the column on its median, and stacks enriched symbols above a baseline with depleted ones below it. That last one is the plotLogolascalls an EDLogo, and it is the only one of the three that can show a symbol which is absent rather than merely uncommon.LogoColumn, whose symbols are arbitrary strings rather than single letters, so amino acid codes, codons and k-mers all plot.LogoTrack::from_sequencesto count an alignment, andLogoTrack::from_matrixto take a position weight matrix.LogoTrack::backgroundfor a non-uniform background,alphabet_sizefor counting symbols that never appear,smoothingfor how loudly absence speaks,symbol_colorfor per-symbol colours, andorderfor which end of the stack the tallest symbol goes.SvgWriter::glyph, which stretches one glyph to an exact box usingtextLength, so the renderer does the fitting rather than this crate guessing font metrics.AxisTrack::center_on_bases, putting each tick in the middle of its base instead of on its left edge. A ruler marks boundaries, but a logo column wants its number underneath it.Theme::cap_height_ratio, the cap height of the font as a fraction of the font size, which is what turns a box height into a font size for a logo.
Changed¶
Themegained a field, so any code building one with a struct literal rather than fromTheme::light()will need updating.
0.1.0 - 2026-07-31¶
First release. Everything below is new.
Added¶
Figure, which stacks tracks over one shared coordinate axis, reserves a label gutter only when a track asks for one, clips each track to its own band, and computes the image height from its contents.Region, 0-based half-open, withRegion::parsefor the 1-based inclusive locus strings used by samtools and IGV, and aDisplaythat round-trips through it.Scale, the shared mapping between base positions and pixels, withx,x_center,pos_at_xand the two resolution helpers.CoverageTrackfor per-base signal, in area, line or bar style, binned to one point per pixel column with max, mean or min, with an optional log scale and a pinnable maximum.SequenceTrackfor the reference bases, drawn as letters, as coloured blocks or as a zoom hint depending on how many pixels a base gets.FeatureTrackfor annotated intervals, with strand arrows, per-feature colours, and packing into as many rows as the current zoom needs so that neither features nor their labels overlap.VariantTrackfor point events, as lollipops scaled by value or as ticks, coloured by category in order of first appearance, with a legend.AxisTrack, whose ticks land on round 1-based coordinates and whose labels share one unit across the whole ruler.Theme, with light and dark presets, the Okabe-Ito categorical palette and IGV-style nucleotide colours, all of it public and replaceable.Track, the trait behind all of the above, so a track type the crate does not ship can live outside it.examples/locus.rs, which renders the two figures in the README from a fixed seed.