Skip to content
chromosomix

chromosomix 0.13.0

What changed in chromosomix 0.13.0, released on 20 September 2026.

  • A box drawn over any chart is a zoom; a double click undoes it (§18.13). Hold ⌘ — or Ctrl, which is the same key on the other two platforms — and drag a marquee over a figure, and the axes come back holding what was inside it. Which axes it sets is read off the shape of the drag, with no mode to be in and nothing to choose first: a flick across the picture sets the abscissa and leaves the ordinate alone, a flick down it sets the ordinate, and a box sets both. The rule is a share of the axis rather than a count of pixels, so it reads the same in a folded drawer and in a full window, and a press that went nowhere is a click. A double click inside the plot puts both axes back to the windows a control last set — the numbers a reader typed, or the whole of the data where they typed none. The wheel still zooms one axis at a time, which is the right gesture for following a curve along; this is the one for arriving somewhere, which used to take a scroll and a pan on each axis, overshooting at every notch.

  • A result matrix is an interactive heatmap (§18.13). A saddle, a pileup, a cis/trans square and a track-correlation heatmap are now drawn by the same chart every curve is: the cells hover and name themselves, the colour bar’s handles clip the scale instead of captioning it, and the marquee and the wheel work on them as they do everywhere else. The colours are still this application’s own tables and the scale is still the painter’s rule, so a saddle in the drawer and a saddle in a paper are the same picture. Contact maps are untouched — a raster of millions of cells is not a thing a heatmap series could ever be — and so is any result matrix above twenty thousand cells, where half a million rectangles in an SVG would be a picture nobody can point at.

    The cells stay square where both axes are the same quantity, whatever the reader did to the drawer, because a saddle is read by comparing its two corners and a corner stretched 40% along one axis looks stronger than the one opposite it.

  • The metaplot’s per-feature heatmap takes the whole width of the card (§18.13). Its rows are a list — one feature per row, strongest first — and nothing is compared across a diagonal it does not have, so square cells only made it a ribbon a fifth of the card wide with white on either side. It now fills the card under the profile it shares an abscissa with, the way every metaplot tool draws it, with that abscissa numbered so the two pictures can be lined up at all. Its colours are clipped at the 99th percentile rather than fitted to the extremes: one feature under an amplification, or a single bin of pile-up artefact, used to take the whole scale and draw every other row at the bottom colour — a flat block with a colour bar running to thirteen hundred. Cells above the clip keep the end colour, so an extreme row is still visibly extreme.

  • Every figure a task produces is now restylable, and the PDF is still a drawing (§18.13). A slim strip over each plot carries the controls reached for constantly — the mark, the two log axes, the legend, Fit — and one button beside it opens the figure in the same dialog the 3D model opens in: the picture at the size of the window, every control in a resizable column beside it. There are new marks to choose from (line, area, step, points, bars; histogram, box and violin as summaries of the ordinate), per-series colours and visibility, typed axis windows, and a title and type size that go into the export. A table is now a view rather than a printing — filter it, sort it, choose its columns, or draw one numeric column as a distribution or against another as a scatter. All of it is remembered per file, per task and per output, beside the parameters the task was run with, and carried in an exported session.

    Contact maps are drawn exactly as they were, by the same rasteriser: a contact map is a raster of up to sixteen million cells with its own gestures and its own export path. A task’s result matrix is a chart now — see below.

    The PDF export is still a true vector page — paths and text, restylable and scalable, not a photograph. The chart library is asked for SVG rather than for a canvas, and the same picture is replayed onto whatever context the export hands over, so the screen, the PNG and the PDF are one drawing rather than three. The library itself arrives in a chunk of its own when the first figure is built, so a reader who never runs an analysis never downloads it.

  • A 3D structure viewer, as a full view of the app (§25.3–25.5). A chromatin model — WashU’s g3d (the text g3dtools takes, or the binary container written here), a .3dg, or bed3d (the same file with an end column) — becomes a row of the Structures card (under Tracks) and a cell of the deck; a .g3d can be dropped on either card, since the loader files it as a structure either way. three.js loads only when a structure cell first opens (a 552 kB chunk, 140 kB gzipped). Its settings sit on a 3D structure card of the Aesthetics panel, right after the Map card, shared with the enlarge dialog’s sidebar: size by a track, a depth slab into a dense model (the fog is tied to it — measured from the centre of rotation, so it actually clips, rather than from the camera), a genomic ruler along each chain, camera presets, a turntable, and the cell’s actions as palette commands (f, a, 1–3, l, r). Drawn-as is three combining icon toggles (beads, sticks, tube); every chain gap is always a dashed run of ink and the gaps option is gone; the cell is always orthographic and lit, so those two options are gone too; fog, chords, names and centre are icon toggles; one ghost opacity slider (0.15 default) covers what is outside the window or ghosted from the key, and outside the window: show draws a whole genome in colour while the maps look at one window of it. A stored style still reads back, the old representation becoming the three toggles.

    Colour by sequence — the key is a control: click ghosts a sequence, double-click shows it alone, the swatch is a colour picker — haplotype, position, the file’s own columns, a loaded track, a feature set, the reader’s marks, distance to the pinned bead, or contacts from the pin, the map’s own row through it drawn as chords above a threshold on the map’s colour scale. Compare with superposes another structure by a similarity fit over the beads the two share, reports the RMSD, and colour by displacement shows where they differ. Hover links every surface: a pointer on a map lights the beads at both ends and joins them (or, with the span modifier held, lights one bead); a pointer on a bead marks every map and tracks cell; a card under the pointer names the bead’s sequence and coordinate; the pointer snaps to the nearest bead within 14 px with a cross cursor while it is aiming; the eye on a Structures row opens and closes the model’s cell in the deck. Marks are three modes, a button and a key each — mark a bead (e), mark a loop (o), measure (m) — replacing a click-counter that had to guess which was meant: a mark is the bead again at twice the radius in the mark’s colour, a measurement a drag between two beads with the model holding still, and select mode turns clicks into manual marks the same way on a map (two beads an arrowhead, offered as chords under your marks). The black lines over the model — a gap’s dashes, a loop’s chord — are half the tube’s radius. A badge in the cell’s corner says what a drag or click does; single-letter keys are bound only while the model is enlarged, so they no longer steal f/a from the Files/Annotations panels when a model sits beside a map. The enlarge dialog’s sidebar is dragged to any width, remembered per file — the cell takes back the camera and colouring the reader left it with. A click pins, Shift-click marks, a double-click goes; bookmarks are named on the model; the export includes a vector PDF of beads and sticks.

    The place under the pointer is always named in the cell’s corner, including on the model itself — one locus there, where a contact from a map is a pair. The sequence box of every structure task is seeded from what the map already shows (the locus inside one sequence, every sequence on screen when it spans several, empty for the whole genome); a model computed again onto the same row replaces the one the cell draws, and removing it empties the cell however it arrived there. Two analysis tasks: model versus map (distance matrices, distance by separation beside the map’s P(s), Spearman agreement per sequence) and spatial tracks (radial position, distance to a locus, local compaction, as synthetic tracks). The deck gains cells that are not maps (§16.11): a tracks cell drawing the session’s tracks alone, and the browser as lead, so a deck of tracks and a structure needs no map at all — the lead’s window ghosts what the maps are not looking at. Demo models ship for the E. coli and yeast maps under demo/.

  • Model from the map builds one bead per bin, with no ceiling (§25.6). The map task that makes a structure from the contacts the view is holding — the whole loaded block, cis and trans, by ShRec3D (contacts as distances c^(−1/α), uncounted pairs filled, classical MDS) with SMACOF refinement, each pair weighted by how often the map counted it (count, its square root, or uniform — count by default; α and the step count exposed, a sequence or typed range restricts it, a table reports the fit) — now builds a bead per bin at the resolution selected. The bins per bead control, and the silent pooling that used to cap a model at the raster’s 4,096-cell ceiling, are both gone: a coarser model is a coarser rung, which reads far less, rather than the one task in the app whose answer came at the window’s resolution instead of the reader’s own choice.

    The bead ceiling is gone with it. Both time and memory are quadratic, so the cost estimate says so — beads and their width, seconds per hundred steps, gigabytes of matrices — and the reader decides; only an allocation the browser will not give is refused, as a named error rather than a dead tab. The run holds two dense matrices where it held six, each built once at its final size; every phase — each rectangle read, each of the eighty sweeps of the classical start, each refinement step — reports progress, so the bar moves and a cancel always lands. A typed locus is priced as the bins of that stretch; over 10,000 beads is refused with the way out.

    The result is a row, not a picture: every model the session holds — dropped, named by URL, or computed — is listed on the Structures card with show in the deck, a bed3d download, the provenance of a computed row, and recompute after a reload; the structure cell gains a model picker, remembered per file, and draws a model the moment it arrives.

  • Track aggregates along a pileup’s two edges (§18.8). The APA takes any number of bigWig or synthetic tracks and averages each over the same windows the pileup is taken from — the real ones, never the shifted controls, which are the denominator of the picture rather than the picture — drawing them as profiles above it and beside it. That is the figure most papers using an APA actually print: the aggregate contact map with the aggregate signal on its edges, which turns these places touch into these places touch, and this is what is bound where they do. Until now a reader had to run the 1D metaplot separately over anchors they re-derived by hand, and the two aggregates were over different windows unless every setting happened to agree. Each profile is normalised on its own — two tracks in two units have no common axis, and what a marginal promises is the shape along the window — and is resampled by the window’s own grid edges, so a cell of the profile is the cell of the pileup above it by construction rather than by a second resampling that could land a domain boundary a column out. MatrixOutput.margins is the general mechanism; the figure counts the bands in the height it asks for, or the top one would be clipped away.

  • The loop spectrum: a called set’s score against how far apart its anchors are (§18.8). The plot neither of the two figures beside it can give, because both average the distance away — a pileup is a mean over windows at every separation the scan covered, and the score histogram counts scores with no regard to where they came from. So a set whose 30 kb calls are loops and whose 2 Mb calls are noise is a plausible dot under a clean unimodal peak. Against distance the two commonest ways a loop set is wrong are visible at a glance: scores that fall away with separation are a detector reporting the distance decay it failed to remove, and scores piled against the minimum-distance cutoff are calls made on the diagonal’s own shoulder. A cloud of points with a median line through it in log-spaced bands — the points are where the calls are, spread included; the line is the trend, in bands rather than a fit because nothing claims the relationship has a shape, and a median because a tail is exactly what is being looked for. Returned by the chromosight detector, by the chromosight pileup and by the ordinary APA over a loop set, where the ordinate is each anchor’s own P2LL.

  • An APA window is cut about the point of the feature the reader chooses (§18.8) — start, centre or end, strand-aware, the same three the 1D metaplot has offered since it existed and spelled the same way. It took the midpoint and never said so, which on a gene set is a pileup at whatever is half way along a gene rather than the figure anybody draws, which is the one at the transcription start site — and a metaplot set to start and a pileup beside it were then aggregates of two different anchor sets under one caption. The chosen point is also what a generated pair’s separation is measured between, or the range and the picture would disagree about which pairs they are of, and the axis label says which point it is: bins from the centre over a pileup of four thousand TSSs is a caption for a figure nobody drew.

  • Previous runs are kept, and there is a way back to them (§18.4). Pressing Run used to replace a result in place, taking the figure, the numbers and the parameters that produced them with it — and nothing about a finished result makes it cheap to produce again: a pileup over forty thousand anchors is a minute of range requests. The runner keeps the last eight runs of each task on each map, and the drawer offers older / newer / latest over them with each run’s clock time and, in its tooltip, which parameter differs from the latest. Everything downstream follows the run on screen — the framing line, recompute, the CLI command, the TSV and the vector export are all that run’s, because a result is a record and stepping back to one means stepping back to all of it. Not persisted, for the reason the framing is a snapshot: a history restored against a file that has since been re-binned would be a list of figures that no longer describe anything.

  • Zoom, as four buttons beside the region box (§5.4). The scroll wheel on the map zooms the picture — the same block, drawn larger or smaller — and past the edge of what was read it stops. This is the other thing a genome browser means by zooming: a different range, re-read, with the region string updated to say so. The four are 10× and 3× out and 3× and 10× in, widest first, so the row reads as one axis of scale with the window you are on in the middle of it, and each is the exact undo of the one opposite. Two sizes rather than one because a single step is always wrong twice — three presses to leave a gene, or one press past the locus. Every press moves about the window’s own centre. Against an end of the sequence the window slides in rather than asking for a negative coordinate; once a press outward would cover the whole sequence it is the whole sequence, named as such; inward it stops at the file’s own bin size; and on a declared circle it wraps past the origin rather than clamping. Each greys itself when there is nothing in its direction to go to, and they navigate through the ordinary road, so three presses and three presses of the back button are the same journey in reverse.

    Zooming out is also what finally made a window across a circular origin reachable: the panel refused to name one window where the axis showed a sequence’s tail and then its head, so the button greyed itself on exactly the view zooming out is most interesting from. Two runs of one sequence is one window on a circle, and it is reported as the region a reader would type — flagged as wrapped, because the Bookmarks card cannot write one as a BED interval and now says so in those words rather than claiming the picture is not at one place.

  • A resolution the file does not store, on the fly (§3.4). A + past the coarse end of the resolution picker asks for one, and what can be given is exactly what is a whole number of some stored rung’s bins added up. That covers both things a reader wants and neither of which a published .mcool reliably has: a coarser picture than its coarsest rung — 10 kb from a file that stops at 4 kb — and one between two rungs, like 3 kb on a 1 / 2 / 5 / 10 kb ladder, which is three 1 kb bins and not a number anybody would have run zoomify for. 2.5 kb on that same ladder cannot be given, because a contact is recorded in a bin rather than at a position inside one, and the dialog says so in those words as the number is typed rather than greying its own button: not a whole number of bins of any resolution this file carries tells you to type 2 or 3. Where several rungs divide the target the coarsest is used — same picture, a twentieth of the stored pixels walked.

    It behaves as a resolution and not as a rendering. The grid is anchored to each sequence’s own start, so the same base pair holds the same sum at every pan and every zoom, which is the grid a real coarse cooler has; the zoom-to-refine ladder takes it as a floor and never reads past it; the whole-genome view takes it as a floor too and its own plan still wins where the picture cannot hold that many cells; and it rides in the link beside balance=, because a 10 kb map and a 1 kb map of one window are different pictures. Which resolutions you have added is a fact about the file, kept per file and carried in an exported workspace beside the sequences you declared circular. An added rung wears a * in the picker — in the label, because the number is what gets quoted in a methods section and this one is not in the file — sorts where its number says, 3 kb before 5 kb, and carries a × the file’s own rungs do not.

    One caveat, printed every time it is offered: counts add up exactly and balanced values do not. cooler zoomify --balance sums raw counts and then balances the coarse matrix at its own resolution; this applies the fine weights and adds the results, which is what every aggregation in this field does and is a good estimate rather than the same number.

  • An APA’s edge profiles are one aggregate over both anchors of every pair (§18.8). Which anchor of a loop is written first in a BEDPE is whichever sits further left on the sequence, and nothing else: it is not upstream in any biological sense, not the cause of the contact, not the end with the factor on it. Profiled apart — which is what the edges did — the top of the pileup was every pair’s second anchor and its right side every pair’s first, so the two lines differed by whatever that arbitrary ordering happened to sort, and a reader comparing them read a difference between the two ends of a loop that the file never claimed. Each window now contributes both of its anchors to both edges, which is one mean over twice as many anchors: better estimated as well as honest. Pooled a window at a time rather than by averaging the two finished profiles, since the accumulators count per cell and a cell one of them never saw would otherwise be weighted as though it had. A tickbox turns it off for the file where the ordering is the figure — a BEDPE whose left column is always the promoter, say — and it is offered only off the diagonal, where the two axes cut the same window from the same feature and the two profiles are already the same numbers.

  • A barChart lane takes a fixed value axis (§8.17). Both readings the format had were relative — every chart against the file’s tallest bar, or each against its own — so a GTEx lane and a knockdown’s lane laid side by side are each scaled to their own maximum, and the one with the quieter tail draws the taller bars and looks like more expression. The lane now carries the same limits row a signal track has, meaning the same thing: auto is what it always did, and a typed pair is an axis the bars are drawn against. It overrides the per-chart switch rather than composing with it, reaches the collapsed glyph so a lane zoomed out does not quietly change what its heights mean, clamps at both ends, and states both of its ends in the line under the lane’s name — which is what makes the comparison it exists for possible.

  • A window may be typed straight across a circular sequence’s origin (§8.15). chrom:2,900,000-100,000 is now a legal region on a declared sequence: cooler refuses an end below its start, correctly, because on a line it is a typing mistake — and on a circle it is the 200 kb either side of the origin, which is where the most local contacts are and is the one window a linear reading cannot ask for at all. The refusal is lifted for exactly the sequences the reader has declared; every other range keeps cooler’s own error, now with a sentence on it saying where the tick is, because a reader who typed one usually meant it. A bad range also stays inline over an intact region box rather than reaching the full-screen error page, which was a long way back from a missing comma.

  • The store grid: Roadmap shipped as a hub, a link to whoever made each shelf, and two doors named for what they do (§24.2, §24.10).

    Roadmap (UCSC hub) is a sixth built-in store, reading WashU’s hub over the 111 reference epigenomes. It is the first shipped store that is somebody else’s hub rather than a file this project generates — no shard list, no stores/ entry, no workflow, no front of its own, just a hub.txt URL and the same fetchStore every store URL already went through. Its rows are read on the gesture that opens it, so the tile costs no request.

    Every store now links to the people who made it, from an arrow beside the download in the drawer’s head: RSG’s team page, IGV’s genome documentation, 4DN’s portal, ENCODE’s matrix, DNA Zoo, Roadmap’s WashU portal, and UCSC’s hgHubConnect on the hub door. A store is somebody else’s data and the rows are a translation of their catalogue, carrying none of the licence, citation or caveats they wrote down; this is the whole of what the viewer does about that. It is drawn as soon as a store is opened, read or not — the reader whose catalogue just failed to load is exactly the one who wants the site behind it.

    The two doors are named for the errand: A track hub is now Connect to a UCSC hub — a hub is somebody else’s shelf, read where it stands, which is what connect means everywhere else on the page — and Your own store is Add a custom store, a file this viewer takes a copy of. The first wears UCSC’s logo, and so does every hub a reader remembers; Roadmap wears its own emblem over UCSC’s colour. The second wears this viewer’s own icon, the file behind it being the one format in the grid written for this viewer rather than translated into it.

    The grid wraps. It was one row that could not, which was right while there were five tiles; there are eight now and a reader can connect more, so it is equal columns that fill the width and spill onto the next line, with the last line’s tiles the width of the ones above them.

    scripts/make-store-logos.mjs grew three things to carry the new marks. An incremental run, so adding a logo no longer means re-obtaining the four whose sources are not committed. A bleed fit for a mark that is already a tile — the app icon is a squircle plate with its own margin and shadow, and containing it on white drew a plate inside a plate. And a crop, for a logo too long to letterbox: Roadmap’s is 958x208, which whole would have set its wordmark at about five pixels on the landing page, so what is taken is the square at its left end.

  • Five UCSC formats, and track hubs as stores (§8.18–§8.21, §24.10). Everything below is one change with one shape: the formats UCSC’s browser reads that this one did not, and the index most of the world’s published data is actually behind.

    interact and bigInteract, longrange and longTabix (§8.18). UCSC’s interaction formats, which are the only ones it has that are about the thing this application is about: two places joined. Both become a PairSet — what a bedpe becomes — so both arrive drawn, as arcs across the matrix and as their own lane in the 1D browser, with no renderer written for either. Trans interactions survive as ordinary off-diagonal arcs rather than as the labelled edge marker a single axis forces on UCSC.

    This is a correctness fix and not only an addition. Both formats are BED-shaped enough to reach the BED sniffer, whose last line is return 'bed', so neither was refused: an interact with a fractional value — UCSC’s own examples write 5.000000 — was drawn as one box spanning both anchors and the gap between them, and an interact whose value happened to be a whole number matched the bedpe rule and was read as one, putting the interaction’s name in the second contig and its display score in the second start. Arcs, drawn confidently, between coordinates that were never in the file. A longrange was a BED4 whose names were the coordinates of the arcs it should have drawn. A longrange’s reciprocal lines — the format writes every interaction twice so that a tabix query from either end finds it — are folded, and the lane says how many it folded, rather than every loop being drawn twice and every count being wrong by a factor of two.

    genePred, genePredExt and refFlat (§8.19). How the gene models of half the assemblies in circulation are distributed, and the gap with a number on it: the IGV store this project publishes was dropping 29 of IGV’s 63 tracks because they are UCSC genePred tables in a .txt.gz, and those 29 are the gene models — the first track anybody wants beside a sequence. They are published now. The same ten columns start at column 0, 1 or 2 depending on whether the file is a refFlat and whether it carries a MySQL dump’s bin index, and eleven columns is ambiguous between the two — so the offset is not guessed from the width but found, by keeping the one where the file agrees with itself: a strand where a strand goes, and two comma lists whose length is the exon count and whose blocks tile the transcript. A bigGenePred already drew correctly, its first twelve columns being a BED12; what its schema now buys is being a gene model rather than a peak set.

    bedMethyl and bigMethyl (§8.20). Base-resolution modification calls, as the quantitative track they are — percent modified per base, with the range, ceiling, autoscale and colour every bigWig lane has. Read as the BED9 they begin with, which is what happened before, they were a lane of grey boxes one base wide with the measurement never mentioned. A file holding two modifications over the same bases draws the more abundant one and says which it did not draw, because those are two measurements and averaging them would produce a number that is in no file. The metagenomic case is the one this was really added for: bacterial 6mA and 4mC are strain-specific and are used to bin contigs, which is what the MAG layer is for.

    .2bit (§8.21). UCSC’s packed sequence format, read by range. One file rather than a FASTA and a .fai — so every way §8.8’s index goes missing simply cannot arise — at two bits a base instead of eight, which is 800 MB for hg38 against 3.1 GB. The gaps are read from the N blocks and painted over the unpacked bases, because the bits under a centromere are not blank but a convincing run of thymine; the mask blocks are stepped over and never fetched, being eight megabytes a chromosome for a lowercase this viewer discards twice over. A long (version 1) .2bit is refused by name rather than misread as wrong residues. The CLI reads one the same way, through the same arithmetic.

    UCSC track hubs, as stores (§24.10). Paste a hub.txt and its genomes and tracks become a shelf: grouped by assembly, narrowed by the hub’s own composite and super-track names, with open with its tracks on every assembly’s heading. An assembly hub opens its own .2bit as the genome — which is why that format is in the same release, since without it every assembly hub opens as tracks over coordinates nothing can read — and a db hub, which carries no sequence, falls back to its chromSizes and then to the first bbi file in its own trackDb — any big* type, a bigBarChart and a bigMethyl being bigBeds with a particular autoSql and nothing else — whose header carries the lengths. It never reaches for a chrom.sizes nobody asked for.

    A hub is recognised from its content at the one function every store URL already goes through, so a hub works in ?store=, in the Connect box, as a remembered tile and in a shared link, and none of those had to learn the word — while a JSON store served from a file called hub.txt still opens as a store. Composite and super-track stanzas, include files, useOneFile on hubs, continuation lines and the indentation subtracks are written with are all read. A track whose type this viewer cannot draw gets no row and is counted with its reason in the store’s description, because a row that misdraws when it is pressed is worse than an absent one. It has its own tile on the landing page, beside the one for a store file.

    The shelf is what the hub asks for. A published hub is not a short list: EPFL’s Breast Cancer Epigenomics hub declares some 440 bigWigs across nine composites and asks for twenty of them. So open with its tracks loads the twenty, not the 440 — parent <name> off and visibility hide are the hub’s own answer to what a reader should arrive to, and a hidden composite hides its children whatever their own word says, which is UCSC’s rule. The verb’s tooltip counts what it will load, and the store’s description says 440 tracks, 20 on by default when the two differ. Every track keeps a row of its own, because off by default is not absent: the reader who came for one particular ChIP still opens it over their own map.

    A hub to try it on ships with the demo. scripts/make_demo_hub.py writes a real assembly hub over the yeast demo data — a .2bit of its genome, the loop calls as longrange, the six bigWigs as a composite and the contact map as type hic — so the whole path can be walked with no third-party host and no CORS question in the way:

    Terminal window
    npm run dev # then paste http://localhost:5173/demo/yeast/hub.txt into the *Add a UCSC hub* tile

    The .2bit that script writes is checked with py2bit, a wrapper around the C in kent’s own tree, so what the reader is tested against is a file UCSC’s own code has already read back base for base — which a fixture this repository wrote could never be. Two loose files sit beside the hub for the formats a hub cannot legitimately carry (UCSC’s remote types are the big* ones plus bam, cram, vcfTabix, hic and longTabix): the same loop calls as interact, and the demo’s GTF as a genePredExt table.

    The IGV store gained the rows this unblocked. Its generator was dropping IGV’s refgene tracks and every .2bit because there was no reader for either; both are read now, so the 29 gene models are published, they carry format: genepred whatever UCSC called the file — so one tick of the Format facet finds refGene, ncbiRefSeq, refFlat and knownGene together — and hg38’s genome row carries its gene track on the shelf where it used to carry nothing.

  • A whole chart inside one feature: barChart and bigBarChart (§8.17). UCSC’s format puts a vector on each row — expCount numbers, one per category — and gene expression across fifty-four GTEx tissues is what it was built for. A dropped .barChart or a bigBed written with the barChart schema now opens as a lane of charts: one bar per category inside the bases the feature covers, named and coloured from the track line where there is one, and numbered where there is not, because the names live in a hub a dropped file arrives without — and the legend says which of the two it is rather than presenting positions as names. It is a FeatureSet like a BED and a VCF, so packing, search, export, the feature card and a metaplot over its intervals are the code a peak set already uses.

    Two things needed deciding rather than defaulting. Where there is no room for bars — a feature narrower than a pixel per category — the glyph becomes one box in the colour of the category that dominates it, at that category’s height, so a lane at chromosome zoom still says which condition wins here long after it has stopped being able to say which ones. And what the heights are measured against: one ceiling for the file makes two glyphs comparable and is the default, but expression is heavy-tailed and against one ceiling the lane is flat except for the genes nobody needed a chart to find — so scale each chart in the lane’s card measures each chart against its own tallest bar, and every glyph becomes a shape. The ceiling is stated under the lane’s label, because a glyph that small carries no axis. A click opens the whole chart, one row per category with its colour, its bar and its number.

    A barChart lane is also the one interval lane with no colour by picker and no arrowheads: its ink is its categories’ and is not a choice, and a control that silently does nothing is worse than an absent one. The demo folder gains Yeast_expression.barChart — real gene coordinates, synthetic numbers, and the file says so on its first line.

  • DNA Zoo is a shipped store. Its open bucket is walked the way any lab’s bucket is — the bucket front end, every object this viewer has a route for, read straight from the listing with no portal in between — and published beside the other four by the weekly Stores workflow. Most of what is in a directory is not a file this viewer opens, so make_store.py dnazoo applies two rules on top of the generic walk. A .hic carries its own index, so a corner of a 3.6 GB map is the bytes it needs and size is no reason to leave one out; nothing else on that bucket has one — the FASTAs are plain gzip with no .fai, the GFFs have no .tbi — so opening one is a download of the whole file, and past --max-unindexed it is a trap rather than an offer. Protein and transcript FASTA route as genome on their suffix alone and are not one.

    Each species’ README.json supplies the rest, and it is worth the request. Asian small-clawed otter is how anybody but its authors looks for Aonyx_cinereus, so the shelf folds under the species with the common name in the heading. The README also names both assemblies, which is why the Assembly column is read from it rather than off the file name: the chromosome-length map is Aonyx_cinereus_HiC.hic under one species and wild_pepper_final_14chr_HiC.hic under another, and a rule built on _HiC is wrong about both ends of that. A species whose README cannot be read keeps every file it has and loses those two columns.

    The tile is a lettermark for now: DNA Zoo’s logo is published under terms nobody here has checked and the file has not been supplied, so it falls back to the coloured square the mark was always designed to fall back to.

  • make_store.py bucket --folders names a bucket’s directory levels. A bucket knows a file’s name, its size and the directory it sits in, and nothing else — so the bucket front end has always written a store with no columns and the folder glued to the note, which on a bucket laid out one directory per species is a flat list of a catalogue that was never flat. --folders Species,Assembly turns those levels into columns the rail filters on, taken from each key’s own path. It adds no knowledge: the directories were already in every key the walk reported, and what was missing was a word for them, which only somebody who knows the bucket can supply. A key shallower than the names it was given leaves the deeper ones empty rather than guessing. Omitted, nothing changes.

  • A store’s catalogue can be taken away with you. There was no way to read the JSON behind a shipped store: a built-in’s URL is never shown on the page, RSG has none at all because it ships inside the bundle, and copy link copies a ?store= link carrying an id rather than the file. A download now sits at the end of the store’s own line in the drawer’s head, and writes the catalogue as the drawer has it — every row before your filters, shards merged into the one file on screen, and every uri made absolute, so what lands opens on its own rather than resolving against wherever it was saved. It is absent, not greyed, while a store is still being read or could not be: a disabled button there would say the file exists and this build will not hand it over.

  • A pasted Drive link opens under the file’s own name. A share link becomes the Drive API’s media address, which ends in an id and says nothing else, so the loading screen, the tab and the router all saw 1Vs9WgmiQj3CMSaDrXNBzzV3tp72GTLu6 where a folder pick would have said SB112b^mapped_CalTech4^B0AGU2.mcool. Drive is now asked what the id is called before the file is opened — one files.get with fields=name, on the same key or token the read carries, once per file for the page — and the answer names the loading screen, becomes an empty tab’s name, and chooses the door by its suffix, so a pasted .mcool opens as a map and a pasted .bw as a browser without the bytes being asked first. A Drive that will not say — a private file this tab is not signed in for, a site built without a key, a network that did not answer — costs nothing and changes nothing: the open goes ahead under the id, as before (SPECS §24.9).

  • A BAM with no index opens, and draws its reads. A BAM was read through its index and nothing else, and a file without one was refused by name with samtools index — the right answer for a file on a disk and the wrong one for the two hosts the store drawer offers most: neither 4DN’s open-data bucket nor ENCODE’s public bucket publishes an index beside any of its BAMs, so every BAM opened from either store answered No index could be read and drew nothing. A coordinate-sorted file is now searched instead: the first read at or after a position is found by binary search over the file’s bytes, each probe a ranged read that inflates a block or two and recovers a record boundary inside, and the probes are kept for the next window. The row’s summary says reads located by search and names the index it did not find; a window then draws exactly what an indexed file draws, to a block rather than to a record, and from 16 kb before its start (SPECS §8.13, docs/hosting.md). A file dropped from a disk without its .bai opens the same way.

  • A BAM whose reads are out of order is refused with the two places that show it. Every BAM on 4DN’s bucket is a Hi-C pipeline’s alignment output, in read-pair order for the pairs step that follows it, with no @HD line to say so. The search holds its probes to the order the file claims, and refuses one that fails — the reads at byte 43,712,010 are on chr2:1,004,120 while those at byte 306,984,070, later in the file, are on chr1:9,512 — naming samtools sort and then samtools index, and saying that the .pairs or the contact map beside such a file is the one to open.

  • The site signs readers in to their own Drive and Dropbox. The Google client id, the Google API key and the Dropbox app key were three boxes every reader had to fill, from a project of their own, before sign in would light up. The published site is now built with the lab’s three, read by the deploy from the repository’s GOOGLE_CLIENT_ID, GOOGLE_API_KEY and DROPBOX_APP_KEY variables — none a secret, all served in the page — so Connect → Google Drive and Connect → Dropbox offer their sign-in at once, and a reader reaches a private folder of their own with one press. The dialog says whose value it is using; one pasted by hand still wins, for a lab with a project of its own; and a build made without the variables is the build there was before (SPECS §24.7, docs/deploy.md).

  • A lane says what it is waiting for. A sequence lane read by range sat blank while its window was on its way, and a BAM’s row said reading… for as long as its thirty-megabyte index took. The worker now reports how far a read has got — a whole file downloading for a row, a window of an indexed sequence fetched by range — and the lane draws fetching 1.2 of 4.0 MB with a bar where its limits would go, on the browser and beside a map alike; the Tracks card draws the same words and bar under a row whose file is still coming. Where the host would not say how much is coming the bar sits half full rather than at nought.

  • A lane that will not draw at this zoom says so. An indexed sequence over a window wider than 4 Mb answers GC under 4 Mb, letters at base resolution — zoom in under its name, instead of nothing. The line is judged over the whole request: a whole-genome view of bosTau9 is thirty chromosomes over it and two thousand scaffolds under it, and the scaffolds were being read one request apiece — a lane that stayed empty for minutes while it did.

  • ENCODE’s genome and file-kind facets are multiple choice. They were radios, so one shard was on screen at a time and the same dataset on two assemblies, or a dataset’s signal beside its peaks, could not be seen in one list. Tick two genomes and both shards are read and shown together, the matrix crosses the union, a link carries the choice as two facets, a shard read once is kept, and a shard not written yet is a sentence beside the rows that were.

  • A large FASTA opens at once, through its index. Opening IGV’s Galgal6 — a gigabyte of sequence — read the whole file into the annotations worker before the browser could appear, or refused it past 300 MB, because a FASTA was the one format held whole: every sequence in memory, which is what made every zoom draw GC content and every pan free. Over 64 MB it is now read by its .fai instead (SPECS §8.8, annotations/fai.ts): the assembly is the index’s first two columns, a window of residues is one ranged read at the byte the index puts it at, and nothing of the sequence is fetched until a lane asks for it — so the browser is on screen the moment a kilobyte of index has arrived, and the sequence loads beside it, a window at a time, in the worker that always held it. The index is <url>.fai unless named with #index= or dropped beside the file, on the terms every other sidecar already had; under 64 MB nothing changes, and a large FASTA with no index is read whole as before, or refused by a sentence naming samtools faidx. What an indexed sequence gives up is what needed the whole of it: its GC lane is blank over a window wider than 4 Mb, the motif search declines it in a note, and an alignment lane colours mismatches only from what the sequence lane has already drawn. make_store.py writes the index into every IGV genome’s row and session, and a lane’s summary says read by range from its index so a reader knows which of the two they have.

  • The store drawer is on the backslash key. A panel at the window’s edge on a punctuation key, as the console is on the backtick: \ folds the stores out and back, is printed on the header button and listed with the command, and stays a backslash inside any box, the drawer’s own search field included.

  • This chromosome is circular — and then the map slides round it. A tickbox in the Map card, on any view that is a window on one sequence (SPECS §8.15). A .cool cannot say that a sequence joins its own ends, and nothing can infer it — a bright far corner is a circular chromosome, or a translocation, or an assembly error — so it is a declaration, kept per file and carried in an exported workspace.

    With it the window becomes unwrapped: it runs off either end of the sequence and comes back on at the other, so a square across the junction is up to four stored blocks, one of which is the far corner where the contacts joining the two ends of the molecule live. Panning past the end reads the same block width rotated round to where you now are — in the square view and under the turn alike, and with no limit but one lap, past which a bin would be drawn twice. Ticks, the coordinate readout, tracks, features and hand-drawn marks survive the join, because the block’s axis carries the sequence twice and says which lap each half is on; a mark is now drawn at every place its locus lands rather than the first, or it would flicker in and out as you slid past the junction.

    And the numbers follow, automatically and with no per-task tickbox: the separation between two bins becomes the shorter of the two ways round, and the longest separation a sequence has becomes half of it. P(s) stops turning back up at the far end — that upturn is the two ends of the molecule wearing a chromosome-long separation, and every fit through it is a fit through an artefact — diamond insulation scores the bins at the junction it used to leave as gaps, and the directionality index, the HiC score and the scalogram wrap with them. What does not yet: compartments, chromosight and the O/E map divide by an expected curve at a linear separation, and a deck companion draws the sequence linearly. Both are stated at §8.15 rather than left to be found.

  • Three more readings of the near-diagonal, and a fourth normalisation for the 4C. The catalogue (SPECS §18.8) gains the directionality index — Dixon et al.’s signed χ², the other answer to the question the diamond asks, with its window in bins like the diamond’s and defaulting to 50, which is Dixon’s 2 Mb at the 40 kb his paper worked at; the HiC score, the sum of every cell in a square window centred on each bin, which is the reading Bignaud et al. use where the question is how tightly a stretch is folded rather than where a domain ends; and the scalogram (Cournac et al. 2012, HiContacts::scalogram()), which is P(s) with one summation left undone — at every bin, the separations at which its contacts reach each of a set of quantiles. All three read a band and never a square, so a 2 Mb window on a mammalian chromosome is a run rather than a refusal.

    The 4C’s Normalisation becomes one control with four answers — none, by coverage, by P(s), by both. Coverage is the other bias and a different one: the decay is a property of separation, coverage is a property of the target bin, and on a raw map a repeat-rich bin is a dip in every profile drawn against it whatever the anchor. The divisor is each bin’s own total contact centred on one, so a raw profile stays in contacts and an O/E ratio stays centred on one. With trans it means the pixel walk can no longer stop at the anchor, and the estimate says the pass is the whole file rather than half of it.

  • The directionality index calls TAD borders and domains. The same two overlays the diamond produces, and through the same kernel. A domain reads as a shape along the index — the 5′ half looks downstream, the 3′ half upstream — so a border is the negative-to-positive flip between two of them. Called flip by flip that is fragile: a track that stutters across zero has crossings a bin apart and only one is an edge, and the rules that pick it are the rules of prominence written out badly. So the index is integrated first. The running sum climbs through every downstream-biased block and falls through every upstream-biased one, which makes each border a local minimum and each stutter a dimple on the flank of the real trough — exactly what callBoundaries already distinguishes, for the diamond, with tests. One implementation decides what a called edge is; both overlays filter on the same column and tile the same way; and the call lands one bin past the minimum, which is the opening bin of the new domain rather than the closing bin of the old one.

    The curve is standardised by the median absolute index before it is integrated, so the prominence is in bins of ordinary directional bias rather than in χ². A χ² grows with how deep the library was sequenced, so a threshold in the index’s own units is a different threshold on every file; this one is a property of the structure, and a value found on one map is worth typing into the next. Dixon’s own HMM differs in one visible way, stated at §18.8 rather than left to be discovered: it leaves stretches it calls unorganised in no domain at all, where borders-between-borders tile.

  • Ribbons: a lane style for a set of quantiles (SPECS §18.12). A ribbon fills the band between its own curve and the curve of the member below it in the same lane, so the scalogram’s five quantiles draw as one nested shape rather than five lines and a legend. Four filled areas cannot express that — they paint over each other in the order they were drawn. Ribbons tile rather than overlay, so they are not dimmed; the lane’s order is the nesting order; a band exists only where both its curves do; and only the top edge of each is stroked, since the lower one is the neighbour’s. A ProfileOutput may now also suggest a display scale, which is what puts the scalogram on a log axis — its values are genomic separations, and on a linear axis all five bands live in the bottom tenth of the lane.

  • Genomic features are drawn as what they are: coloured by strand, pointing the way they run, and banded so the two strands are two places. Every interval lane — a BED, a peak set, a bigBed, a gene model, a VCF — used to be one flat colour with the direction hidden in a tooltip, which meant a screenful of boxes said where and nothing else. Three changes, on all of them at once, because they are one renderer (§8.16):

    • Strand is the default colouring, in three colours: forward keeps the colour on the lane’s own chip, reverse is that hue turned most of the way round, and a feature whose file carries no strand takes the grey this app already draws an N in and already gives a variant with no single base behind it. The two hues come from the reader’s own colour rather than a fixed blue and red, so four peak sets on one screen are still told apart by the colours they were given — the same argument an alignment lane’s categories already make. A file with no strand in it — a peak set, most BEDs — keeps its one colour rather than turning entirely grey, since a grey meaning “no strand” says nothing where nothing has one.
    • A stranded feature is pointed. The head is cut out of the leading end rather than drawn past it, so the glyph covers exactly the bases the feature does and what is clicked is still what is drawn; a long one carries chevrons through it as well, so a segment spanning the window says its direction at every point. Below six pixels there is no head, because a pentagon three pixels wide is a rectangle that has lost a pixel. A gene with exons keeps the chevrons on its spine and takes no head — a point on its last exon would be a point on a piece of the gene rather than on the gene.
    • The rows are banded: unstranded, then forward, then reverse, outward from the map. Two genes on the same bases and opposite strands used to be two boxes one above the other with the difference carried entirely by colour. The bands are read from the file rather than from the window, so panning never rearranges the lane, and whether they fit is asked of the lane’s height rather than of the rows a label needs — so a segments lane at its default sixteen pixels, which is exactly where a stranded BED lands, bands into two rather than never banding at all. Below eight pixels a band would be a hairline, and the lane falls back to the packing it always had.
  • A drop-down on every interval lane, to colour the segments by any column the file carries. source, biotype, signal, -log10 q, QUAL, filter, score — whatever the parsers wrote. The keys are read back out of the joined attribute string each feature carries (§8.3’s decision, and this is what it costs), sampled across the whole file rather than its head, cached per set and bounded at twenty thousand rows — so a picker over a four-million-line annotation opens as fast as one over a BED with nine lines in it. A categorical column gets the same colour-vision-safe palette a metadata strip uses, handed out commonest-first with a twelfth-and-beyond other bucket; a numeric one gets a viridis ramp fitted to the file, never to the window — a colour that meant one thing before a pan and another after it is a legend that lies without saying so. The key sits in the card under the picker, built from the same function the renderer calls, and the chip on the lane’s name becomes a colour that is actually in the lane.

    A choice whose column the open file does not have is kept and marked rather than quietly corrected, because the same session opened against the other half of an experiment should colour by biotype again the moment the GTF comes back. Nothing here refetches: a feature set is in memory whole and a field is a column of it, so a dozen answers cost one repaint between them.

    Alongside it, a point strands switch for the readers and the figures that want the plain rectangles back.

  • Public data stores: IGV’s genomes, all of 4DN, all of ENCODE, and sessions with the tracks already on. The two shipped portal stores listed contact maps and nothing else — 1,200 rows of 4DN and 200 of ENCODE — and the filter that did it was on the experiment, which needed editing every time a consortium published something new. The rule is now on the file: if this viewer has a route for the name, it gets a row. So the shelves are:

    • IGV genomes (new). The reference genomes IGV ships, read from the same igv.org.genomes/genomes.json the IGV desktop app and igv.js load their menu from, plus every annotation track published beside them. Each URL is probed with a ranged GET — the request the viewer itself makes — which records the size that table does not carry and settles whether the file opens without credentials. Two sets of rows fall out: IGV’s 29 refgene tracks are UCSC genePred tables this viewer has no reader for, and six genomes’ sequences are not public at all — igv-genepattern-org answers 403 AccessDenied anonymously, which takes out IGV’s own hg38 and hg19 (GRCh38 stays on the shelf through IGV’s hg38_1kg, on the public 1000genomes bucket). Both are named in the run’s log rather than shipped as rows that fail when they are clicked.
    • 4DN open data, ~25,700 rows where it was 1,200: the bigWigs, peaks, pairs, bedGraphs and BAMs as well as the maps.
    • ENCODE, ~210,000 rows across twelve shelves. ENCODE’s manifest holds 1,209,805 released files this viewer can open; the default breadth keeps one per dataset, assembly and format — every dataset it has — and each row says how many files it stands for. --breadth full and --breadth all publish the other two answers.
    • Preconfigured sessions, three shelves of them. A row is a whole saved session: pick IGV hg38 and the sequence and its gene track are on screen together; pick a 4DN experiment set and its richest contact map opens with the bigWigs and peak files of the same set on it. This needed no new code in the viewer — a store row could already be a workspace (§24.1) — only a generator that writes the files.

    The drawer offers all of it as four tiles rather than eighteen menu entries: the ENCODE shards are the Genome and Files facets of one ENCODE page, IGV’s sessions are the button on each genome’s card, and a portal’s sessions are an action on a group of rows rather than a shelf of their own. A list draws a window of a thousand rows while the facets count over all of them — 126,000 rows of eight cells is a million elements and a tab that stops on every keystroke. Nothing is fetched until a tile is pressed, which is unchanged and is why eighteen files cost what three did.

    scripts/make_store.py grew an igv front end and reads two sources per portal instead of one: a spine that says which files exist, from a place that cannot be incomplete (4DN’s bucket listing, ENCODE’s daily encode_file_manifest.tsv), and an enrichment that says what each one is of, from one lean portal search keyed by accession. The old portal-only 4DN query embedded the experiment set’s processed files and not the per-experiment ones, so it was missing about nine files in ten and said nothing about it. --portal require|optional|skip decides what an unreachable portal means; --sessions-dir writes the sessions. .github/workflows/stores.yml publishes all of it, and two things about the stores branch changed with the volume: it is force-pushed as one orphan commit (100 MB a week as commits is five gigabytes a year for a store whose only interesting state is its tip), and nothing changed is finally a real answer — compared on a fingerprint of each file with its timestamps removed, rather than on bytes that could never match. SPECS §24.3 has the measurements and the reasoning; docs/stores.md is the operator’s page.

  • Stores: a shelf of named datasets, and any lab can publish one. A store is one JSON file beside the data (kind: "metacooler.store", SPECS §24): maps, genome files and whole workspaces with columns to filter on — assembly, biosource, assay, lab — so a reader chooses without opening anything. The app draws each as a page of its own in the store drawer (under Changed below) and ships the lab’s own — the four published figures, with the data files under them — beside the public ones, regenerated weekly from the portals by .github/workflows/stores.yml and published beside the app by the deploy. Paste a URL under Your own store to connect a lab’s, or arrive on it with ?store=<url> beside ?config= and ?embed=. A row may be a workspace, which is what a pre-configured session is; a row carries its size so the cost is on the table before the click. Nothing is fetched on load: a store is read when it is chosen.

    scripts/make_store.py writes one: 4dn and encode from the portals’ own search APIs (every processed file with an open-data URL, no experiment-type filter, so Micro-C is in), and bucket from a prefix on any S3-compatible store — the catalogue docs/hosting.md always promised. The motivation, the measurements and the comparison with juicebox.js are in docs/remote-sources.md: the unmodified viewer already opened a 2.2 GB 4DN .mcool in 7.8 s over 13 requests; what was missing was a name for it.

  • Links that were not yet the file now open as pasted. A Dropbox share link is read from the content host; a Google Drive share link becomes the Drive API’s ranged form (a public file needs the lab’s API key, kept beside the bucket keys; a private one, the account below); gs:// and storage.cloud.google.com become the GCS JSON-API form, which answers CORS for any origin; and s3://bucket/key resolves through a bucket scope that names the bucket, else to the virtual-hosted URL. The rewrite runs wherever a URL becomes a URI, so a shared link already carries the working form (src/sources/rewrite.ts).

  • Presigned S3 URLs, signed GCS URLs and Azure SAS links open, until they expire. The HEAD probe that a single-method signature refused is skipped when the query carries a signature, and the one-byte range probe the reader already had takes its place. docs/hosting.md no longer says otherwise.

  • Sign in to Google to read private Drive files and GCS objects. The lab supplies a client id registered for the app’s origin, as it supplies a bucket key (Connect → A Google account); the token lives in this tab’s memory, is never written down, and rides every request as a header; a 401 or 403 from a Google host offers sign in on the error screen instead of the CORS explanation, and retries the open once it succeeds. The dialog says the one thing a consent screen buries: drive.readonly is all of the reader’s Drive, where a bucket scope is one prefix. Absent in the standalone build, which has no origin to register, and in the desktop build, whose app:// origin Google will not register either; the API-key half stays in both. Nothing here could be exercised against Google from where it was written — docs/hosting.md ends with the five-step check to run once, and it is the one item of this release still to be confirmed by hand.

  • A Google Drive folder and a Dropbox, as connected folders. Both join the Files column behind the same ConnectedFolder interface as a disk and a bucket (SPECS §24.9): one level at a time, a thousand entries, then truncated. A Drive folder — My Drive, or a folder named by link or id, shared drives included — is listed with the signed-in account, folders first, Google-native documents left out; a picked file opens at the same API address a pasted share link does, and the map takes the file’s name. Dropbox is the one provider with an account and no key: the lab registers a scoped-access app, the reader signs in through PKCE with the code Dropbox shows and the reader pastes — no redirect, so it works on any origin, the desktop’s included — and the whole Dropbox the app was granted is browsable; a picked file is a temporary link, unsigned and valid four hours, which is what a shared address then carries. Tokens live only in this tab; the refresh token Dropbox offers is refused. Neither could be exercised against its provider from where this was written; docs/hosting.md carries the manual check for each.

  • The README says what already works. Object storage, not one vendor: R2, MinIO, Azure with a SAS, GCS through its JSON-API URL and the 4DN and ENCODE buckets all opened before this release; docs/hosting.md ends with the table, host by host.

  • A console, along the bottom, holding the commands your day was made of. Every analysis run is now recorded the moment it runs, written as the metacooler-run … command that repeats it — the same line the drawer’s copy CLI command button hands over, from the same provenance, so the two cannot drift. Press the terminal mark in the header (or `) and the log comes up as a drawer over the bottom of the window: a shadow, a draggable top edge, and Escape to put it down. Nothing about it is modal — every shortcut keeps working behind it, and closing it stops nothing.

    Run on all maps writes one line per map. A batch over a deck of six is six runs against six files, and the drawer can only ever show you one of them, so five of those commands used to exist nowhere. Each row names the file it measured and how long it took, and a run that failed or was stopped keeps its line with the reason on it — which is exactly the run whose parameters you want back.

    Beside the runs: the files you opened, the tracks you loaded and every file the app wrote. Not navigation — a minute of panning would bury the rest.

    What copy and save hand over is a script, not a transcript: commands bare, everything else commented, days as headings. A day pasted into a file runs, in the order you ran it, with the afternoon around it left in as context. Each day heading carries its own copy button, because “the commands I ran on Tuesday” is the question this is for. save writes the same text as a .sh.

    The output toggle is the choice between a script and an archive: off, a row is its command; on, each row is preceded by the full provenance block — build, task and version, framing, every parameter — which is the header every exported file already opens with. It governs the clipboard as well as the screen. The caveats a command carries about what a shell cannot be told are never hidden by it.

    Three things the command writer had been getting wrong surfaced the moment a whole afternoon of runs was on one screen, and all three are fixed where they live — so copy CLI command in the drawer is fixed with them. A run in a 1D browser session has no contact map, and the command opens one as its first argument: it used to name the bigWig that declared the assembly, which reads perfectly well and dies at the open, and is now refused by name with the button greyed to match. A parameter whose load is its own flag — motif calls its reference sequence — wrote --sequence ref.fa --sequence ref.fa and now writes it once. And a session framed against the assembly is named by its file rather than by its whole address, so a bucket URL no longer fills the drawer’s heading and a console row.

    Kept in this browser for seven days or five hundred lines, whichever comes first, and nowhere else. A run the window walked out on comes back marked interrupted rather than as a failure nobody saw. Forget everything drops it, and the storage dialog names it on its own line rather than counting it among the settings — it is a list of the files you opened, and you should not have to press the button to find out it is there. An embed has no console at all.

  • Every pull request gets a preview on the site. A branch of this repository opened as a pull request is built as a pinned copy and published at …/pr/<number>/ beside the archived pushes, replaced on every push and removed when the pull request closes; the About dialog lists the open previews in a group of their own, and the pull request carries one comment with the address. The preview job writes the archive branch and asks the deploy to publish, so the site keeps one publisher (docs/deploy.md, “Pull requests get a preview”).

  • Five new analysis tasks, and two existing ones grown a question they could not answer. Every one of them is a measurement the app could not produce rather than a rearrangement of one it already draws, which is the line §18.9 draws and the reason each is argued where it is built.

    Reproducibility, across the maps on screen. The deck opens N files under one navigation, blends two in a square and overlays their curves — and every one of those answers do these look alike with a picture. The question people actually have is are these replicates, and its answer is a number. HiCRep’s stratum-adjusted correlation (Yang et al. 2017), ported: both maps smoothed with a 2D mean filter, each diagonal correlated separately — where the decay the two share is a constant and carries no variance — and the strata pooled by the Cochran–Mantel–Haenszel weight. Plain Pearson on two matrices comes out near 0.9 for two unrelated maps of the same assembly, which is why the stratifying is the method rather than a refinement of it. Reads a band rather than a square, so a mammalian chromosome is a few dozen bounded reads. Three outputs, and the third says more than the scalar does: the N×N coefficient matrix, the per-sequence table, and correlation against separation, which tells these agree near the diagonal and not far from it — a depth difference — from these disagree everywhere, which is two experiments. A map on a different bin grid is refused by name, because a stratum is a diagonal offset in bins and §16’s lock is in base pairs on purpose.

    Per-cell contact QC on an .scool. That container’s whole interaction is one decision — which cells to sum — and the Cells table could only rank them by nnz and sum, which is what the writer recorded and a statement about the sequencing rather than about the contacts. Depth is also the least interesting axis a single-cell library varies along: cells differ mostly by where in the cycle they were fixed, and a pseudo-bulk across phases smears exactly the structure somebody opened the file to see. Per cell, from its own pixels: contacts, cis fraction, and the share in each of four separation bands — Nagano et al. (2017)’s, whose near-against-mitotic scatter is the cell cycle drawn as a horseshoe. It costs what summing the same cells costs, to the read: the same reads in the same order, one accumulator per cell instead of one shared. The numbers land back in the Cells table as sortable columns, because the gesture they are for is the one that table already has — sort, tick a run of rows, sum them. No phase is assigned: the bands are a measurement with a citation and a cut through the horseshoe is a method with choices.

    Cis and trans per sequence, with the sequence × sequence square beside it. The first number any Hi-C paper quotes, per sequence rather than per bin, and the off-diagonal read against what the two sequences’ own trans coverage predicts under indifference. Above 1 is a translocation, a fusion, a centromere cluster or a Rabl configuration. It is the inter-MAG network’s own accumulation with the group being a contig rather than a genome — and it is not a translocation caller, for the reason §18.9 gives about any second caller.

    Fraction of signal in features — ChIP-seq’s FRiP, over any loaded tracks and any interval set. The covered fraction sits beside it and the ratio of the two is the column to read: thirty per cent of the signal in thirty per cent of the genome is a track with no preference at all.

    A shuffle null under interval overlap. Jaccard, shared bases and features hit are all descriptive and none has a scale, so how many of my loops sit on a boundary — the question §18.8 gives as the task’s whole reason — was not actually answerable. bedtools shuffle’s null: one set relocated at random within the sequences it already occupies, keeping its count and its lengths. Reported as a fold, a z and a permutation p on the tail the observation fell on, with Phipson and Smyth’s +1 so the p has a floor instead of coming out at zero.

    Every pair of a feature set, and pileups split by separation. APA gains a third anchor mode: rather than a loop set somebody produced, every pair of one feature set inside a separation range — which is the test a loaded BEDPE cannot be, since a BEDPE is already a claim about which pairs matter. And either off-diagonal mode may be split into log-spaced separation bands, each with its own pileup and its own control, because one pileup over 30 kb to 2 Mb is an average of a strong short-range contact and a weak long-range one.

    The four pair orientations, beside P(s). Read from the records during the import’s own pass and at record resolution, because binning sums the four together and destroys the one thing they are for: the separation at which ++, +−, −+ and −− converge is the shortest one a library can be believed at, and it is usually below whatever grid the reader chose. A .pairs that was never imported can be named in the form and read for its records alone. §3.7’s nothing about strands is read is narrowed in the open: its reason was filtering, no strand reaches a bin or a pixel or the written .mcool, and the pixels are identical whether the columns are there or not — which is asserted rather than claimed.

  • A lane can draw its values as colour, and multi-scale insulation does. heatmap joins line, area and bars in the style picker: the value becomes a colour across the whole lane instead of a position in it, so it costs no height. A lane whose tracks all draw that way divides its own thickness between them and stacks them, keeping the height the reader set — two translucent lines in one lane are legible because ink adds, but two translucent colour maps are a third colour that means neither.

    Diamond insulation at several windows now arrives as exactly that: one group, overlaid, widest window at the top, sharpening downwards, so a boundary that survives every scale reads as a column through the stack. It used to arrive as four lanes of crossing curves on four autoscaled axes. A run with a single window is unchanged.

    The colours are read from the data unless you choose: values that straddle zero get a diverging map centred on the zero, values that do not get a ramp. The per-lane picker sits beside the style and appears only when the style is a heatmap.

    Two smaller things came with it. A stacked lane writes a name per row, in the row it names, rather than one line of four names across whichever row it lands on; and the Synthetic tracks card carries the style picker, which it had to — it is the only card that lists a computed row, so a style offered only on the Tracks card was one an insulation lane could never be given.

  • A comparison of two tracks is a synthetic track. Combining two loaded bigWigs used to open a row on the Tracks card, among the files it was computed from — the one card with no exports on it, so there was no way to get a ratio out of the app at all. It lands on Synthetic tracks now, beside what the analysis panel computed, with the provenance line and the three exports every computed track carries: bigWig, bedGraph and TSV.

    The lane itself is unchanged and still lazy: it cuts at every boundary of either operand, so the step function it draws is exact at any zoom. A file has to be on some grid, so the export evaluates both operands onto the map’s own bins — the same grid every other synthetic track is written on — and says so in its header, because a reader comparing the file against the lane should be told which of the two they are holding. Where a lane brings x / 0 back onto its axis, the file writes no value: an infinity cannot go in a bigWig.

  • Each track’s lane has a scale: linear or log₂. Beside the drawing style, on the Tracks card and on Synthetic tracks. It is a display scale and nothing else — the numbers behind the lane, and every number that leaves it as a file, are untouched — and the logarithm is taken of the pixel, not of every interval under it, because a log axis draws log(mean) and not the geometric mean. A value at or below zero has no logarithm and is drawn as a gap, so log₂ suits a ratio and not a difference.

  • A pileup’s axes are numbered in base pairs, not in bins (§18.8). The APA, chromosight’s pileup and the chromosight APA all drew a picture whose axes said Bins from the centre and carried no numbers at all. They now read Distance from the centre — or from the feature’s start or end, whichever the window was cut about — with five labels along each axis: -21 kb, -10.5 kb, 0, +10.5 kb, +21 kb. A count of bins is a number only a reader holding the resolution in their head can use, and a different number for the same window at every resolution. A scaled window keeps its own 0 → 1 labels, its axis being the feature rather than a distance.

  • The Structures card has no box to paste a URL into either (§5.11). It arrived with the 3D card after the Tracks, Contact maps and deck rows were cleared of theirs, so it kept the one pattern every other card had lost: a drop zone with or open one by URL folded under it. The rule is the card’s shape rather than a list of cards — what a card that asks for a file needs is the drop zone and nothing under it. A structure held at an address still opens by every road it did: the landing page’s own box, a saved session, a #uri link, or the Files column.

  • A sequence is declared circular in the Chromosomes table, in a column of its own (§8.15). It was a tickbox in the Map card, which was the right card for what the flag changes and the wrong place for what it is: a declaration about the assembly — which of these sequences is a plasmid, which is the mitochondrion — asked one sequence at a time, of whichever one happened to be on screen. Declaring three of them meant opening three maps, and there was nowhere at all to read which had been declared. The column sits last, after every column the file itself carries — it is the one column that is not in the file, a declaration the reader makes rather than a number somebody measured, so it goes where a reader’s own additions go rather than interrupting the run of the file’s own fields. The filter row’s own tick narrows to the sequences that carry it, which is what makes the question answerable on a draft assembly with three declared contigs in a hundred thousand; and the Sequences card in the General panel wears a ↻ beside each declared name, so a reader at work is told the answer rather than offered a second place to click. A map already on screen adopts the change without being rebuilt, so the reader keeps the zoom they needed in order to be somewhere the question was worth asking from.

  • The resolution picker is a slider over labelled stops, at the far end of the top bar (§5.5). It was a dropdown of raw bin counts — 1000, 5000, 50000 — sitting second in the trail, between the file name and the table. Three things were wrong with that. It read as a step of the trail, which a rung of a zoomify ladder is not: it is the same file drawn more or less finely, and the crumbs after it stay where they were. It was in bins, and 50000 is a number a reader has to divide before it means anything. And a dropdown hides the ladder, where a slider over stops shows how many rungs there are, which one you are on and which way is finer. The stops now say 1 kb, 50 kb, 1 Mb; they are real radio inputs, so the arrow keys and a screen reader get the list; and the slider commits on release rather than per rung, since dragging across five rungs must not re-open the file five times. An mmetacool’s entry picker is untouched and stays before the root, because switching to a different nested metacool genuinely does invalidate every crumb after it.

  • The landing page is one screen with three doors, not one long scroll with two. Load files is a shallow drop target — Drop genomic files here / or click to choose, with an or a URL box under it — and the redundant choose files button beside it is gone, since it opened the same dialog a click on the target already does. Mount a local or remote folder is new: four buttons side by side, choose a folder… (the directory picker itself, one gesture, no dialog in front of it), connect a bucket…, sign in to Google…, sign in to Dropbox… — the last three opening the Connect dialog straight on the pane they name. These places used to be reachable only from the Files column’s own plug, a page a reader arriving with a folder had not looked at yet; the Files column now appears only while a place is connected and disappears when the last one is, since a row of buttons on the landing page made an always-open, always-empty column redundant — the doors that remain to it are the landing page itself and Open a local/remote folder in the command palette. The desktop build shows three of the four buttons, a Google sign-in having no origin to register against app://viewer. Connect to a store is the third door, unchanged in kind.

    The masthead’s tagline and the drop area’s own paragraph are gone — between them a fifth of a laptop screen — and each of the three headings now leads with the icon that thing already wears elsewhere (a file, folder-tree, library), with 4em between the three sections so they read as three offers rather than one column of paragraphs. The page sits on a dotted ground — a sub-pixel dot every 10 px at half the border grey — the only patterned surface in the app, so arriving at and leaving the landing page are both legible. Each store tile’s blurb is written for the roughly fifty characters its two lines actually hold, rather than truncated to them, and a remembered store shows the host of its URL (builtin-stores.test.ts holds the budget).

  • One button, everywhere. Every .btn takes the same gesture on approach — the border and the word to the accent, a one-pixel lift, a shadow — which was the store tiles’ hover and is now the rule rather than the one place in the app that had one. The quiet ones (a close cross, a link, a chip, a tab’s ×) opt out of the lift, having no surface to raise. The landing page’s row of places and the two sign-ins stop being .btn-primary: they stood directly above a row of store tiles offering the same gesture, and four blue blocks over eight grey squares read as a toolbar over a gallery — as well as claiming, against the doctrine, that one of the four was the answer. None of them is. They are tiles now, and .btn-primary is back to meaning the invited answer in a dialog.

  • The mark has a track down its left side and another under it, which is where this application puts a track: the lanes grow towards the matrix from the bottom and the left edges. Both rise at the first bin of each of the three blocks and disagree in between, which is what having two of them is for.

    They are slivers in the margin the plate already had — a third of a pitch across, a fifth of a pitch clear of the map — and one hue at two saturations, not two hues. The first attempt gave each lane a whole row of an 8 × 8 grid and a colour of its own, and it cost more than it bought: three hues on one plate is a generic analytics icon and breaks the rule this application is built on, a solid run of seven cells outlasts a sparse diagonal at every downsample so at 24 px the decoration became the identity, and the extra row took an eighth off the map, which is the subject. Drawn this way the field keeps the size, the spacing and the palette it has always had; the one thing the lanes ask of it is half a lane’s footprint of optical centring, so the mark rather than the map sits in the middle of the plate. Redrawn from scripts/make-app-icons.py, so the favicon, the install icon, the iOS icon, the dock icon and the custom-store tile are all the one drawing.

  • The greys are neutral, and the borders carry the contrast. The whole ramp had a blue cast — #c8cdd4, #98a1ac, #4a5058 are all blue-greys — and a blue-grey chrome around a blue accent leaves the accent nothing to be. They are neutral now, in both themes, and lighter in the light one: --bg-sunken is #f5f6f8, and what got darker instead is --border and --border-strong, which were pale enough that a card read as floating on nothing and a table’s rules disappeared on a projector. A hairline that cannot be seen is a missing hairline; a fill that cannot be seen is just the page. Two elevation tokens with it, --shadow-1 for resting on the page and --shadow-2 for lifted off it.

  • The HiC score is the mean of the bin’s own line across the near-diagonal, which is what the R the score comes from computes. It was the sum of every cell in a square window, and neither the shape nor the missing division was right.

    The shape. The score is now a[k, k + d] for |d| ≤ D − 1 — the bin’s column, reaching D − 1 bins each way — where it was every cell of a square centred on the bin. Drawn on a map that is a straight line through the diagonal: half a crosshair, one arm-pair, because the matrix is symmetric and taking both arms would count every off-diagonal contact twice against a main diagonal counted once. Every cell in it touches bin k. A square window does not have that property: it also counts the self-contact of k − 1 and the corner joining k − 1 to k + 1, cells that involve k in no way — and on a bacterial map at three diagonals those borrowed cells are most of the score, so consecutive bins shared half their cells and a single bright bin came out as a three-bin plateau. On Bignaud2025_T7.mcool a bin whose neighbours sit at 55% of it under the line reads 87% and 89% under the square.

    The division. It is a mean now: N = 2D − 1, the cells in the line. A sum grew with the number of diagonals and could only be compared against another run of the same size on the same map; a mean is contacts per cell and a lane labelled in contacts is readable as contacts. Multiplying by N returns the R script’s own number exactly — which is how this is checked: against Bignaud2025_T7.mcool::2000 at NC_000913.3:3,442,000 the kernel returns 3.7433 × 1/N where that script returns 3.7433, and the same at every neighbouring bin.

    The parameter is a count of diagonals, counting the main one, replacing a window in base pairs. It defaults to 3 — the main diagonal and the one either side, five cells, two bins each way — which is the bacterial reading at the 2 kb resolution the score is computed on there, and is the R script’s num_diagonals = 2 under the other way of counting. P(s)’s minimum separation and the diamond’s window went the same way and for the same reason: the near-diagonal is a fact about the grid rather than about the genome, and the same typed distance meant three diagonals at one resolution and one at the next. The runner says what the reach came to in base pairs on the file open and how many cells are behind each point of the track.

    The read is shallower: the band is D − 1 deep where the square needed twice its own reach for its corners — two bins at the default, one strip on most files, no tiling.

    A bin without a whole line through it now scores nothing. Dividing a short sum by a full N fades both ends of every linear sequence towards zero, which reads as this end of the chromosome is unstructured — the artefact the diamond has always refused partial windows for, and one that arrives the moment the reading becomes a mean. A circular sequence has no end to run off and every bin on one is still scored. A cell the balancer dropped inside a whole line is a different thing and is unchanged: counted rather than guessed at, never scaled up, and NaN below minValid.

    The task’s version is 3, so a result carried in from a shared session is not silently compared against one computed here. --window is gone; the parameter is --diagonals, and an old command line naming the old one is refused rather than quietly reinterpreted.

    One thing worth knowing and not fixed here, because it is a property of the data rather than of this task: at three diagonals the main diagonal is the bulk of the line, it is where self-ligation lands, and on a balanced map it is the one place the weights were never fitted — cooler excludes ignore_diags diagonals from its own solve and then scales them anyway. On the E. coli map above, the bin under that peak carries a balancing weight 1.45× its neighbours’, so its self-contact is scaled 2.09× and the peak is in the weights rather than in the counts, where neither statistic peaks at all. ignoreDiags = 1 cuts that cell out of the score, and the hint on the control now says why one might.

  • An explicitly planned pooled read is snapped to the sequence’s own grid. The reader anchors the cell grid to each sequence rather than to the window asked for, so two blocks of one region agree cell for cell — but it did that only for the plans it chose itself, and a request carrying its own binsPerCell skipped the step. That is why the zoom-to-refine module carried a standing note saying it must never send one: the cell values would have moved as the reader panned. Both paths snap now, which is what makes a declared resolution above a resolution rather than a rendering. A no-op for the two callers that sent an explicit plan before this — a whole-genome read and a selection both start at sequence boundaries.

  • Nine resolution rungs are chipped rather than six (§5.5). A zoomify ladder that doubles from 1 kb reaches a megabase in ten rungs and one on the 1–2–5 series reaches it in nine, so the old cap collapsed most real .mcools to a bare slider — and collapsed an eight-rung one the moment somebody added a ninth, which is exactly the reader the + above is for. Nine stops is about three hundred pixels of short numbers, right-aligned into whatever the trail leaves.

  • Every dialog’s body is inset from its panel, and no dialog has to remember it. Each one names its own body class and each used to set its own padding — a rule spelled a dozen times, and the way that shape fails is not subtle: a dialog written without the declaration comes out with its text against the panel’s border, looking broken, and nothing says what was forgotten. (The resolution dialog shipped exactly that.) The padding is stated once now, against the panel’s own children, at a specificity that beats a body class. Three are flush on purpose and opt out by name, in one place where the exception list can be read whole: the command palette’s search box and the feature card’s footer each carry a border across the panel’s whole width, and the stage (§18.13, §25) is a picture at the size of the window beside a column the reader drags the width of — where an inset would be measured into the drag, since the grip sizes the column from the modal’s own right edge.

  • The region bar loses its back arrow and its map/tracks cycle button (§5.4). Both were controls with a shorter road already beside them. Going back is the browser’s own back, which this bar’s navigation has fed since it existed. Tracks only / map only / both is the eye beside the map in the column that lists what is drawn — where every other thing on screen is shown and hidden, one row at a time, by the control that names it. A second way to do a thing is not a convenience when the first way is a row the reader is already looking at; it is another control to learn, in the bar that has to stay readable at a glance, and the bar has just gained four zoom buttons. The v key still walks the three states, because a key costs the bar nothing.

  • A barChart lane’s ceiling is dragged like a track’s (§8.17). Giving the lane a fixed axis left the reader one way to move it — typing two numbers into the card — while every lane above and below it answered a drag across its body, and a ⌘-scroll over it, by moving its ceiling. A lane that answers that gesture with a pan, in a stack where nothing else does, is a lane the reader has to be told about. Both gestures now reach a chart, on the map’s stacks and in the 1D browser, and they start from the axis already on screen — zero to the file’s own tallest bar, the same number the typed field seeds from, so a notch and a drag on one lane cannot begin from two different places. What this cost is an invariant that had held since the kind predicates were written: has a movable axis and is drawn as a profile are two questions, and they were one function’s worth of answer only for as long as nothing but a profile had an axis. A chart still gets no row of track controls — no autoscale group, no mode or flip switch — because it is not a measurement in anybody else’s units.

  • An APA’s half-width starts at 21 bins rather than 12, and Anchor on loses its paragraph (§18.8). Twelve was narrow enough that a loop filled the picture: the corners a P2LL divides by were a couple of bins from the peak rather than background, and a reader had nowhere to watch the pileup sit back down to it. The paragraph above Centre / Start / End explained which end of a minus-strand gene is its 5′ end, over three options that carry a line each — three lines of the card spent saying what the options say, and the 1D metaplot has asked the same question with no such paragraph since it existed.

  • The three or open one by URL boxes, and the from a connected place… button that briefly replaced them, are both gone. Each URL box was the worse half of the pair it stood in: a reader holding a URL can drop the link on the window or paste it into the landing page’s own box, and a reader without one — the commonest state in all three places these stood, since the bigWigs beside a map are keys in a prefix and a repaired shared deck’s ~ has no address a page could fetch by name (§11) — was being asked to produce something they have not got. What that reader needs is to be shown what is there: the Files column, open for exactly as long as a place is connected (§20), rather than a from a connected place… button that opened the same folder view in a dialog from under every such card — four doors to one room, each closing on the first pick, where the way people actually work through a directory is to open one, look, open the next, and keep going. The column itself is untouched, a drag out of it still lands anywhere a drop lands, and the drop areas’ hints are cut to the formats each zone takes and nothing else.

  • The store tiles carry real marks, in one row. RSG’s tile is a contact map of its own — 100 bins square in the viewer’s own default ramp, with domains, compartments, loop peaks and the decay to paper every Hi-C map has, RSG rasterised onto the diagonal as bins like the rest of the picture (npm run icons:rsg, committed as an indexed PNG whose palette is the default ramp, held against applyColormap('default') by a test so it cannot drift out of step with the colours maps are drawn in; the letters sit behind the image, so a mark that fails to load is a lettermark rather than a hole) — where all four tiles once shared a coloured square with initials in it. The other three now carry their own logos too, IGV’s, 4DN’s and ENCODE’s having since been given to this viewer to redistribute: each is fitted to a 160-pixel square (npm run icons:stores; ENCODE letterboxed, since its badge is half again as wide as tall and would otherwise crop the word ENCODE away; 4DN composited on white; IGV full bleed) and committed as a data URI, the standalone build emitting no public/ for either set. The coloured grounds stay underneath every tile as what shows while an image loads or if one fails, deliberately nobody’s brand.

    The row itself no longer wraps at most widths — the tiles share the width instead of spilling a lone extra tile onto its own line — and the thin coloured arc that showed through each mark’s antialiased corners, reading as a border the tile does not have, is gone now that the fallback colour is dropped the moment an image sits in front of it.

  • The 4DN and ENCODE stores list a BAM only where its reads can be drawn. A third of the 4DN store’s rows were BAMs, and most of those were clicks that could only be refused: 2,271 were the alignment files of Hi-C-family assays (in situ Hi-C, Dilution Hi-C, Micro-C, Capture Hi-C, PLAC-seq, DNase Hi-C, ChIA-PET, MARGI, TCC, HiChIP), every one bwa mem -SP5M output in read-pair order with no @HD line, which the viewer refuses for want of a sort, and 6,309 were BAMs the portal describes nowhere — no assay, no biosource, no dataset, nothing to filter on. The generator now leaves both out, so bam in the Files facet is the 1,397 alignments of RNA-seq, ChIP-seq, CUT&RUN, Repli-seq, DamID and TSA-seq, which are sorted and draw; the .pairs and the contact map of every Hi-C set are the rows that were wanted from it, and they stay. ENCODE’s alignment shards lose their Hi-C pipeline’s files the same way — 471 rows of in situ, dilution and intact Hi-C, @HD SO:unknown — and nothing else: ENCODE’s ChIA-PET alignments are ChIA-PIPE’s and coordinate-sorted, and a BAM the portal did not describe keeps the cells its manifest row has. The rule is about what a BAM is — its reads — and not a list of assays: a new member of the family whose name says Hi-C arrives filtered, and a new linear assay arrives listed (SPECS §24.3, docs/stores.md).

  • The Connect dialog is four panes, each a title, a ? and one button. Every pane used to carry two or three paragraphs of explanation between its heading and its control — the Drive pane had a second heading of its own, a bucket pane a public / needs a key switch that asked the reader to classify a prefix before describing it — and the three provider panes (this machine, Google Drive, Dropbox) also carried boxes for the site’s own client id, API key and app key. Those boxes and the storage behind them are gone: the three are the site’s own registrations, built from GOOGLE_CLIENT_ID, GOOGLE_API_KEY and DROPBOX_APP_KEY at deploy (docs/deploy.md), so a reader is offered a sign-in and nothing to configure, a site built without a registration says so where the sign-in would be, and the About screen and stored settings carry nothing for them. What is left on each provider pane is one line — Connect to your Google Drive, Connect to your Dropbox — a ? at the far right holding what the grant is, where the token lives and how long the browser keeps it (reachable by keyboard, read by a screen reader as the button’s description), and one large sign-in button with the provider’s mark on it; the Drive pane’s folder box appears under the button once signed in and goes away on sign-out, and the Files panel names the tile Google Drive rather than a section that no longer exists. The bucket pane keeps a name, a URL and Additional settings: whether a scope is signed now follows from whether a key was typed into all three of region, key id and secret, rather than the reader declaring public or needs a key first, and its ? carries the CORS policy and the Range/Content-Range pair a .mcool cannot be read without, as blocks to paste rather than prose to translate back into one. The Files column’s own button and the dialog it opens are both named Open a local/remote folder, in place of connect a place… (SPECS §20, §24.7, §24.9).

  • IGV’s genome cards are four across, tighter, and named without the bracket. Human (GRCh38/hg38) is Human with hg38 as its chip — the assembly was on the card twice — the whole name staying in the tooltip and in what the filter box searches; the grid is four columns at the drawer’s default width, two when it is narrowed.

  • A store row is called what the drawer called it. A 4DN map opened from the shelf was named by its accession — 4DNFI24F7GRY.mcool — where the reader had picked Hi-C on HFF cells, protocol variations. The row’s line now goes with the file: onto the map, onto the track, and onto the tab a row opens in a session of its own, through the same road a file named by a Drive folder takes.

  • The lab’s figures and IGV’s genomes are one shape of card. The name and the card’s one verb on the first line — the new-session mark, icon only — the chips under them, the one-line description where there is one, and 8 files behind a fold listing every file the session is made of, each a store row with the two verbs. The Figures heading and the separate Files section are gone: every file is under the figure it belongs to, and the few no figure names sit under the cards in a fold of their own. IGV’s cards lose the host line under the name.

  • Every file row carries the same two marks: add it here, or open it in a new session. A store row had add as a word and open as a grey arrow, a folder row in the Files column had the arrow alone and add only in the tooltip, and which of the two a row offered depended on whether the tab was empty. Now every row — a store’s, a folder’s — has a + that joins the file to the session in front and a window with a + where its corner was that opens it as a session of its own, both icons, both always there, the words in the tooltips (SPECS §20.5, ui/rowVerbs.ts). The same mark stands beside open on a gallery card and open with its tracks on a group, and the tray under a portal gained add to this session beside open together. A workspace row has the second alone, being tabs and not a file.

  • Two verbs to open anything: load files, or connect to a store. The landing page had grown a URL box, a browse…, a private buckets…, a dropdown of eighteen shelves and a list of examples, and the header a plug, a + and a Buckets card that overlapped all of them. Every one was one of two things under another name. The page now says the two (SPECS §24.2): Load files is the drop area and the URL box, and Connect to a store is a row of tiles — the lab’s own figures, IGV’s genomes, 4DN, ENCODE, every store the reader connected, and a + for one by URL.

    A tile opens the store drawer, a panel down the left in the family of the Analysis drawer and the console: resizable, put down by Escape or its header button, and back where it was left when it is reopened. Each store gets the page its data wants. The lab’s store and IGV’s are galleries — one card per figure or per genome, a line of context, one button that opens the whole pre-configured session, the files under a fold. 4DN and ENCODE are portals: a rail of facets (assay, biosource, lab, format…) whose counts follow every tick — OR within a facet, AND across them — a search box, a matrix that crosses any two facets and opens a cell, and a tray that collects rows to open together. What is open is in the address — ?store=4dn&assay=Hi-C&biosource=HFFc6&view=matrix — so a narrowed store is a link, and ?store=<url> opens a lab’s own. Nothing is fetched until a tile is pressed.

    Around it, the doors are fewer and each says what it is. Connect (the Files column’s Open a local/remote folder) is a row of providers — this machine, a bucket, Google Drive, Dropbox — with each provider’s credentials on its own pane rather than a bucket card here and a Drive key there. The file crumb’s Open a file opens one Load dialog with the store tiles behind it. Every URL box that once had a browse bucket… beside it has from a connected place… instead, which lists every place the window is connected to and not only buckets. The Files panel is the one listing of connected places. And scripts/make_store.py writes a Group, Experiment set or Dataset column into what it generates, which is what the galleries and the sessions are grouped by.

  • A map of several ticked genomes is divided and labelled by genome. Ticking four MAGs in the genomes table asks for four blocks — their own maps on the diagonal and everything between them off it — and the picture came back ruled and named by contig, so the thing four ticks were spent on had two thousand hairlines across it and no line anywhere saying where one MAG ended and the next began. The whole-metagenome view has always refused exactly that wash; a selection of genomes is the same picture of a chosen few and now reads the same way.

    The rule it follows is the one the whole-file levels already state, one level down: what was ticked is what the axis is divided and labelled by. So ticking contigs is unchanged — there the names picked are the contigs, and coarsening to the genomes behind them would throw away the answer to “what did I pick” — and so is a single genome, whose own contigs are the only structure inside it. The genome of a contig is still on the tooltip in every one of those cases, as it was; what changed is only which of two run lists gets the ink.

    Where the axis is pooled — enough small contigs and a cell holds several — a cell is now kept inside one genome, the same guarantee the whole-metagenome scan already makes for its slots. Without it a boundary can land a cell late and the block it closes carries a sliver of the next MAG, which is precisely the kind of invented adjacency the pooling rules exist to prevent.

    And clicking one of those labels opens that genome. It used to open a region view of it — a legal destination, since a bare genome name is a range the parser takes (§5.4), and one screen short of where the gesture meant to go. The whole-file picker has always gone straight to the genome; both now dispatch on what the axis actually is rather than on which view is asking, so the two pictures that are divided by genome behave alike. A label on an axis of contigs opens a region, as it always has.

    A companion panel in a deck follows the lead here, because the decision is made on the read rather than in the view: #genomes= shows the same blocks in every map it is applied to.

  • A GTF’s biotype is written into the feature’s attributes with its key in front of it (biotype protein_coding, where it was a bare protein_coding). An attribute with no key is a fact nothing can ask for — invisible to the new column picker — and the feature card behind a click gains a label it was missing anyway.

  • The number under a domain pileup is no longer P2LL. Peak-to-lower-left measures a dot against the corner furthest from it, and the lower-left corner of a domain window is the upstream neighbour against the downstream one — two regions further apart than anything inside the domain — so the ratio is mostly the distance decay and reads well above 1 on a set with no boundaries in it at all. What is reported instead is in / out: the domain’s own corner against the same block just across its boundary, which sits at very nearly the same separation, so what is left when you divide is whether contact stops at the edge. Above 1 is a domain. The per-feature table carries the same number per domain, and the loop pileups are unchanged — they still report P2LL, which is the right statistic for a dot.

  • Observed / expected is refused in the scaled mode, in the form rather than at Run. One cell of a scaled pileup is fed by windows at a dozen different separations — that is what scaling is — so it has no one diagonal offset for a P(s) to be looked up at, and dividing by one anyway produced a plausible picture of nothing. The refusal names the alternative: the distance-matched control, which is drawn at the same separations by construction and is what the task defaults to.

  • The scaled pileup’s axes are labelled in units of the feature — 0 at its start and 1 at its end — with the flank the run used named beside them, instead of an unlabelled Scaled position.

  • A store or a hub can be pasted instead of fetched. Both doors now carry a folded or paste the hub / or paste the store box, with an optional base URL for relative names. Two reasons, and the first is not convenience: a store is fetched by the page, so the host has to allow a cross-origin GET, and plenty never will — genome.ucsc.edu among them, which puts UCSC’s own example hubs out of reach of every browser-only viewer. The file is public, the reader can see it in another tab, and the only thing in the way is a response header on somebody else’s server. Pasting gets past that without routing anybody’s traffic through a proxy this project would then be asking them to trust (C1). The second is writing one: a lab assembling its own shelf edits, pastes, sees what broke and edits again, and publishing a file to a bucket between each of those is a loop nobody runs twice.

    One box and not three, even for a hub, which is three files. useOneFile on is a hub in a single file — and a hub.txt, genomes.txt and trackDb.txt concatenated are the same thing, because the only keys the concatenation loses are the two saying where the next file is and the next file is already in the box. A text that is only a hub.txt is refused by name and told what to paste after it, rather than coming back as an empty shelf. Relative names are refused with the first of them quoted, so the base box is an obvious fix rather than a shelf whose rows fail one at a time when they are pressed. A pasted shelf has no address, so it is not put in the address bar and not offered for remembering: a store worth keeping is one with a URL.

    Each box carries a fill in an example button beside open what is pasted, and what it puts there is a hub and a store that actually read — over the published demo files, so the first press draws a shelf and the reader’s first edit is to a thing that already worked rather than to a template whose state they cannot tell. The hub example is an assembly hub, naming the demo’s own .2bit through twoBitPath: the rung of §24.10’s ladder worth demonstrating, and the only way a hub carries a sequence of its own. It is a hub somebody wrote and checked against the bucket rather than one composed here, which is why Yeast_reference.2bit is published beside the FASTA and carries that name. It asks before replacing a box that is not empty, because that box is where somebody’s half-written hub lives. A test pins every file the two examples name against the published set, since an example that 404s on its first press is worse than none.

    The CORS failure now names the box, because a reader staring at Failed to fetch has no reason to guess there is one — and where the host is a UCSC one it says what was measured on 2026-09-16: neither genome.ucsc.edu nor hgdownload.soe.ucsc.edu sends the header, so UCSC’s own example hubs and the whole of GenArk are out of reach of any browser-only viewer, this one, igv.js or otherwise. That is a UCSC hosting decision rather than a fault in the reader’s URL, and saying so beats sending them to check a link that is perfectly correct. It also says that pasting such a hub’s text gets past it for the hub and not for its bigDataUrls, which are on the same hosts — the part that would otherwise be discovered one row at a time.

  • A store’s own name replaces the door’s on the drawer heading once it lands. A door is named for the errand — A track hub, Your own store — which is right until one has been read, at which point the page is about a particular shelf and the heading says which, with the store’s own description under it. Both doors, not only the new one.

  • Every task in the catalogue runs from the shell, so every run the console records is a real command. metacooler-run refused four of them by name — the 1D metaplot, track correlation and FRiP, whose bigWig reader lived in the annotations worker alone, and reproducibility (HiCRep), whose second map the deck supplies — and any run made in a 1D browser session, which has no contact map to open as the first argument. The console (§23) recorded each of those as a sentence with the refusal under it, and the drawer’s copy CLI command was greyed. Now --tracks opens a bigWig, a bedGraph, a wig or a FASTA read as GC content where it lies, and the runners are handed the same sample the worker answers; --against names the maps HiCRep compares with, mounted as the analysis worker mounts them; a track task with no file positional runs over the assembly the tracks declare, framed as the session frames it; --track names the file behind an id a replayed workspace recorded, and the tab’s own track list says which is which. Two silent gaps went with them: compartments --phaseSequence had parsed the reference and then reported that it could not be read, because the per-bin GC vector the session ships was never computed in a shell; and expected --stranded could not read a named .pairs from disk, nor the orientation curves a .pairs given as the map had just counted. A composite (magnetwork) runs what it requires first, at its defaults, as the app does. And three facts of a framing the session sets from the picture, each an input to the arithmetic and none a parameter of any task, are now flags the console writes whenever they differ from the shell’s defaults: --value for the score column a .metacool is drawn from, --circular for the sequences declared circular (§8.15), and --cell for the cells of an .scool that were ticked (§3.8). An imported .pairs writes its --bin-size too, since a contact list declares no resolution and the line died at the open without it (README, SPECS §23).

  • A legend overlay. The key in the corner of the map — the score, the bin size and the two ends of the colour scale — has a toggle of its own in the Map card, on by default, beside the dividers and ticks it stands with. Off is for the figure whose caption already says it, and the caption now does: every export ends its detail line with colour −3.13 to 0.385, so a map without its key, or two maps side by side, are still read against their ranges.

  • The whole-genome gate names the cheaper rung. Pressing W on a fine resolution used to read every stored pixel and say so afterwards; the ask-first screen now says in a line what it costs and, on a laddered file, offers open at 250 kb instead — the coarsest rung whose bins still fit in one drawn cell, which draws the same picture from a fraction of the pixels. A .hic declares no pixel count and was never gated at all; a file that states no count is now gated on its bin count instead (WHOLE_VIEW_GATE_BINS), so a chicken genome at 1 kb asks before it walks every block.

  • Compartments say when the resolution is wrong for them. 4,642 bins on one sequence at 1 kb is under the correlation gate and six minutes of eigenvectors with the CPU meter reading idle. The line beside Run now says so in red, gives the rule of thumb — 250 kb on a mammalian genome, and past about 2,000 bins the correlation takes minutes — and names the rung of this file at which the widest sequence would fit (open it at 5 kb (929 bins) or coarser). The analysis context carries the file’s resolution ladder for it.

  • Annotations and Analysis start folded. Five open columns beside a square plot left the plot a thirty-pixel sliver on a laptop, with the whole genome button clipped off the chromosome table. A window that has never chosen now opens with the two columns a reader reaches for least on arrival folded to their tabs; a stored choice still wins, and the open state is written as one, so a reader who opens a column keeps it open (ui/SidePanel.ts, defaultFolded). The Files column folds itself, for this window only, once a file chosen or dropped on the landing page has opened — it is the one column that page shows, and it has done its job.

  • Restore adds to what is open. The banner outlives the picker, so a reader who opened a file while it stood and then pressed Restore had the file they just opened replaced by the ones they had, with no message and no undo. A window holding anything now takes the remembered tabs beside it, the way a dropped workspace file always did; only an empty window is replaced.

  • The Export card exports the deck. Its download button and the S key called the lead panel’s own figure, so a reader with four maps side by side got the first cell of the arrangement — the mismatch ⌘C had already been cured of. Both now go through the deck when there is one: every panel at the card’s dpi, laid out as on screen, as a PNG or as a one-page PDF; a deck of one map exports as that map, vector PDF and all.

  • The Tracks card says when the lanes are put away. V cycles the map with its tracks, the tracks alone, and the map alone, and the third stop left the card listing eight files drawn nowhere — which read as the tracks having been lost, and was reported as the square map losing them. One line above the list now says where they are and that V brings them back.

  • The readout under the pointer says what and where. count 0.003 under a key reading Balanced was two names for one number, and 1.22 Mb × 1.23 Mb named a ten-kilobase neighbourhood of a cell one kilobase wide, so two cells three bins apart printed the same string. The tooltip and the status line now use the score picker’s own word and write the coordinate out in full — NC_000913.3:1,215,000. The analysis drawers’ selects no longer shrink to their first four letters (Chro, ever, Prob).

  • A pileup’s edge profiles were drawn from zero, which flattened them (§18.8). A ChIP aggregate running from 45 at the baseline to 92 at the anchor — a doubling — came out as a line across the top of a band three lines tall with a small bump in it, while the same numbers as a metaplot curve two blocks down the drawer were a peak twice the height of its baseline. Two pictures of one quantity disagreeing about whether anything happened, and the arithmetic was right in both. Each profile is now drawn against its own extent, with the two edges of one track sharing a ruler — they are one measurement — and the range printed beside the track’s name in the key, which is what keeps a self-scaled band honest about how much of an effect it is showing.

  • A pileup’s edge profiles now leave as numbers, with the matrix they belong to (§18.8). The TSV a reader exports carried the cells and not the lines drawn along them, which made the one thing in the figure with no numbers behind it. They are written as a second block under the matrix — contacts and signal are two units, and a spreadsheet reading them as one rectangle would average them together.

  • metacooler-run apa --tracks opened the file and then reported that it could not be read. A parameter that takes several entries files each one under key:entryId, which is how the app’s runner reads them back; the shell filed one vector under the bare key and read the parameter as a single string, so a pileup asked for at a command line came back with a note saying one of one tracks could not be read — about a file the command had just opened. The convention is now written down once and imported, rather than stated in two places.

  • A matrix figure’s colormap control drew empty on exactly the figures that set one. The slim strip offers six colours inline with the full list in the dialog, and a select whose value is not one of its options draws blank — so a saddle’s RdBu_r or a pileup’s fall made the control look broken. Whatever is in force is now offered alongside the six.

  • Fourteen rules in the stylesheet drew nothing, and looked like they meant to. Six names — --line, --muted, --fg, --fg-quiet, --bg-inset and --warn — were read across the sheet and defined nowhere, neither in CSS nor from the code. An unresolvable var() is invalid at computed-value time, which does not skip the declaration: the property takes its inherited value if it inherits and its initial value if it does not. So border: 1px solid var(--line) came out border-style: none and drew no border — on the colour ramp beside an annotation, on the share dialog’s link box, under the analysis drawer’s map tabs and round a plot control’s number box. background: var(--line) came out transparent, so the well behind a feature-count bar and the one-pixel divider in the plot controls were invisible. And color: var(--muted) inherited, so two labels meant to sit quietly under something else were drawn at full strength. Each of those looks, in a screenshot, exactly like a deliberate choice, which is why none was found.

    --warn is now a real token in both themes, and the three rules that wanted a warning colour stop inventing one each: .pairs-warning was written var(--warn, var(--accent)) and therefore drew a warning in link blue, and the other two hard-coded ambers that never changed in the dark theme. .input.is-bad takes --danger like the .input-region.is-bad three thousand lines above it, instead of a red of its own that stayed dark on the dark theme’s dark surface.

    test/unit/css-tokens.test.ts is why there is not a fourth time. It reads every var() in the sheet and fails, with the line number, on any name nothing defines — allowing only the sixteen properties the code writes with setProperty, and checking that allowance against the source so it cannot rot. It also holds the two halves of the dark palette to each other and the light palette to the dark one’s set of names, which is how --tab-rest came to exist in four of the six blocks that carried a palette and in neither of the other two.

  • The palette is written once. The light values were spelled out in full in four places — :root, [data-theme='light'], .content and .analysis-figure — and are now one selector list that names all four. Eighty-four declarations become sixteen, and the two boxes that stay light while the furniture goes dark can no longer drift from the theme they are a copy of. The dead var() fallbacks went with it: var(--accent, …) appeared with three different fallbacks for a token that is always defined, and fifteen font sizes bypassed the type scale for values that were a step under another name.

  • A bacterial GFF arrived as 87 features out of 5,870. A 6.4 Mb Pseudomonas aeruginosa PAK annotation — 5,783 CDS, 74 tRNA, 12 rRNA, one tmRNA — opened as 87 anonymous boxes: every RNA in the file and not one of its proteins, each drawn with no name and no accession. The count is the diagnosis. 74 + 12 + 1 = 87, so what came through was exactly the rows whose feature type ends in the letters rna.

    The parser only knew the hierarchy GTF and an Ensembl GFF3 are built from — gene, transcript, and the exons and CDS underneath — and it keyed every row that was not a gene or a transcript by its Parent, because in that world such a row is a fragment and the parent is the thing being drawn. A row naming no parent could not be placed, and was skipped. A bacterial annotation has no hierarchy in it at all: Prokka, Bakta and DFAST write one row per feature, each with its own ID and locus_tag and not one Parent anywhere in the file, because a bacterial gene is a single uninterrupted CDS and the gene and mRNA rows Ensembl writes would each be a second copy of the same span. So all 5,783 CDS rows went looking for a transcript they had never named and were dropped, while the tRNAs and rRNAs matched the “is this a transcript” test on their type and came through on their own IDs. That the survivors were a plausible-looking 87 features rather than an error is why this read as a file that had loaded.

    A row that names no parent is now read as the whole feature it is. It is keyed on its own ID — so two rows sharing one, which is GFF3’s way of writing a feature with a gap in it, are one glyph — and a row carrying no ID at all is drawn keyless rather than collected with every other such row into one feature stretching across the file. Where a file does say what a row belongs to, that still decides and nothing changes: a CDS with a Parent still folds into its transcript and is not drawn beside it.

    Three things that were being thrown away come with it, because 5,870 nameless boxes is not much better than 87. locus_tag now counts as an accession — it is assigned per gene by definition, and it is the only one a Prokka file writes — so the 2,643 hypothetical protein rows that carry no symbol are labelled IMMMIJEE_00005 instead of nothing, and the search box can find them. A bare gene attribute now counts as a symbol, which is how NCBI and Prokka both spell one, so dnaA, gyrB and 3,139 others are drawn under their own names. And product joins biotype and source in the detail string: it is the whole of what a bacterial annotation has to say, the feature card had been showing source Prodigal:002006 and stopping, and the colour picker (§8.16) can only offer a column that is written there.

    One row type had to be turned away deliberately as a result. NCBI opens every contig with a region spanning it end to end, and a GFF converted from GenBank writes source for the same thing; both name no parent, so both used to fall off the same end the CDS rows did. Drawn, each would be a box the width of the chromosome sitting on top of the genes.

  • A species with no README.json no longer fails the whole Stores run. --readme optional is the default precisely because those files are hand-maintained across hundreds of species, and it promises that a species without one keeps every file it has and loses two columns. Against recorded fixtures it did exactly that; against the bucket it could not, and the first weekly run of the DNA Zoo store died on Preliminary/ — a top-level directory that is not a species and never had a README. Nothing was published that week, so the shelves readers saw stayed as they were.

    The cause is one fetcher doing two jobs. A portal search is read by a function that treats 404 as zero results only when the body is JSON — 4DN’s way of saying a facet matched nothing — and turns every other status into the end of the run. A bucket refuses a key that is not there with a 404 and an XML body, which landed in that function’s “the endpoint has moved” case. Reading a document that may simply be absent is now its own function, and it answers no document to any status and to a body that is not a JSON object. Absence and outage are still told apart: a host that cannot be reached, or that stops talking part-way through three times over, still ends the run, because a store is only ever replaced by a run that succeeded. --readme require still fails on the first missing one, which is what it is for.

    A run where no directory yielded a README now says so in a line of its own. Every row is still published and the run still succeeds — but a bucket that answered for its objects and for none of its READMEs has more likely been rearranged than lost every one at once, and two silently empty columns are not a thing to find out about from the shelf.

  • A shared link opened the picker on the first visit in a fresh browser profile. Not on the second, and not in a second tab, which is what made it read as a broken link rather than as what it was. The first load of the site registers the service worker; the worker calls clients.claim() as it activates, so it takes over the page that is still running; and the page reloaded, because a worker that has replaced another is serving a page built from the one it replaced. A worker installing for the first time has replaced nothing — the page came off the network a moment earlier and is already the newest build — but it fired the same controllerchange and got the same reload. openSharedHash decodes a session and then takes #session=… out of the address bar before the tab it describes has finished opening, so the reload landed on a bare URL and the session was gone. The first claim no longer reloads; an update still does, which is the case the reload was written for.

  • A store that did not answer with a store said it was not a saved workspace. The store reader borrowed the workspace reader’s wording for the one refusal both formats share, so a host answering with a page instead of the file — which is what a dev server does for a store the Stores workflow has not generated — produced … is not a JSON file, so it is not a saved workspace, naming a kind of file nobody in the drawer was trying to open. It now says it is not a store, and names the case that causes it.

  • Analysis tasks on a map imported from a .pairs failed at the mount. §3.7 says every §18 task reads an imported map through PixelStore exactly as it reads a cooler, and that was true of the reader worker and not of the analysis one: it opens the file for itself (§18.3), reached MetaCooler.open with a contact list, and was refused with choose a bin size — a dialog the reader had already filled in, on a map that was on screen and working. The binning now travels with the job, and the second mount re-reads the records once, behind the bar and the cancel the first pass had. The pairs road moved into worker/open.ts, which is where that module’s own docstring argues the knowledge of how many roads there are into a map belongs — the HiC-Pro defect it was written for, a second time.

  • A curve output can draw as points. Every other figure in the catalogue is a function of a separation, a position or a rank, so joining consecutive samples says something true about what lies between them. A cell-cycle scatter is two fractions measured on the same cell with no order along x, and a line through it would draw a path nothing travelled.

  • Diamond insulation’s estimate counts the reads it makes. It still said one dense square per sequence, which is what it read before the diagonal was tiled: four range requests for a chromosome that is now read as a run of squares along its diagonal — on mouse chr1 at 5 kb, a number two orders of magnitude under the truth, on the one screen a reader consults before starting something expensive. The estimate and the runner now share the arithmetic instead of each carrying a copy.

  • A lane too thin for two lines no longer runs its range into its name. The range goes under the name where there is room for it and beside it where there is not, and beside it, it started exactly where the name ended: 80 kb and -1.46 read as one word. Rare while every lane was 90 pixels tall, ordinary once four heatmaps share one lane.

  • Both scans on the map read a diagonal instead of a square, and the fine resolutions they used to refuse now work. Neither the insulation diamond nor the chromosight correlation has ever looked at a cell far from the diagonal — a diamond reaches window bins each side of the bin it scores, and a correlation runs inside a band max_dist wide — but both read the whole n² square to get at the band, and the ceiling that fell out of that had nothing to do with either computation:

    • insulation hit the 2 GiB limit on a single Float32Array, which is a limit of the tab and not of the machine. That is 23,170 bins: 116 Mb at 5 kb, so mouse chr9 was refused and chr1 could not be run below 10 kb.
    • loop calling never reached the allocator. A hard-coded 8,192-bin cap skipped any longer sequence, which is 82 Mb at 10 kb and 41 Mb at 5 — so mouse chr18 was dropped at 10 kb and every mouse chromosome at 5 kb.

    Insulation now walks each sequence as overlapping squares with a halo the width of the widest diamond. The split that makes this a substitution rather than an approximation is between the sums, which are local to a bin, and the score, which is a log2 ratio against the mean over the whole sequence: each tile files the sums for the bins whose whole diamond it holds, and the score is taken once at the end. diamondSums is untouched and still goldened against cooltools; the answer is identical cell for cell, not close. Mouse chr1 at 1 kb — 195,471 bins, which is 153 GB as a square — now runs in 19 seconds inside a 4 GB heap.

    Loop calling reads the band it was always going to scan. chromosight.ts had already banded every one of its own objects, saying so in as many words; the square read was the last n² thing in the module and the gate was the fence built around it. The gate is now a bound on the band — the scan distance times the sequence — so it scales with the setting that actually costs: the whole mouse genome scans at 5 kb, and what is still refused is a deep scan on a fine grid, refused by name with the two settings that fix it. Mouse chr1 at 5 kb, a 6.1 GB square, takes 68 seconds.

    One thing genuinely needed the square and now comes from the pixel table instead: the detectability cut counts non-zero pixels per bin over a whole row, at every separation. It is counted once per run and only where the file carries no balancing weights — with them nothing is read for it at all.

  • A sequence a scan skips is named. The cap’s own documentation has always said a sequence past it is “skipped and named”; only the first half was true, so a run over a mammalian genome scanned whichever chromosomes happened to fit and reported a clean number for the rest. Worse, the message when nothing fitted said the sequences were not long enough, when the truth was the opposite.

  • metacooler-run --sequences binds. The runners read that parameter with Array.isArray and fall back to every sequence for anything else; the shell passed a plain string, so --sequences chr18 silently scanned all of them and reported a number from the wrong one. Affected loop calling and compartments.

  • The domain pileup — APA over a set of TADs, scaled — was laid out from the wrong end of the feature. The scaled mode on a feature set is how you draw the picture that sits beside a loop APA in every Hi-C paper: call insulation boundaries, take the span from one to the next as a domain, and pile every domain up stretched to the same width with padding either side. It read the window as [midpoint − flank, midpoint + length + flank] — the right width, and half a domain to the right of where it belongs — so the body sat against the top-left corner of the picture with the whole flank on the far side. The window is now [start − flank, end + flank] on both axes, which puts the domain in the middle and its two boundaries on the cross through it.

    Nothing in the old figure said it was wrong, which is what makes this worth a paragraph rather than a line: a shifted domain pileup is still a square with bright and dark blocks in it, and the only way to catch it was to know where the corner peak should have been.

  • Every domain’s boundary now lands on the same grid line. A scaled window used to be stretched evenly onto the grid, so a 20 kb domain’s edge fell on one column and a 200 kb domain’s on another — averaging them softened both boundaries into a couple of columns of gradient, which in an aggregate looks exactly like a real boundary that is soft. The grid is now explicitly flank, body, flank: the body’s two ends are pinned to their own columns for every domain whatever its length, and the flank each side is a fraction of that domain rather than a rounded number of bins.

  • A distance-matched control is shifted by a multiple of the window when the window is a scaled one. Asked for in base pairs — the old control, and its 100 kb floor — a control for a megabase-wide domain window was ninety per cent the same cells as the thing it was the denominator of, so the ratio came out at 1 everywhere and the domain washed out of the picture with nothing on screen to say why. The fixed mode still asks in base pairs, which is the right unit for a window a couple of dozen bins wide.

  • A scaled window off the diagonal — a loop pileup whose window scales with the separation — is now symmetric about its anchor bin rather than a bin short on one side of it.

  • metacooler-run takes the ::/resolutions/<n> address its own refusal tells you to write. An .mcool is a collection of coolers, and opening one at its root is refused with a sentence naming the resolutions and an example ending in ::/resolutions/25000 — advice the shell could not then take, because the whole string went to stat and there is no such file. So there was no way into an .mcool from a command line at all. The :: half is now split off for the filesystem and kept for the container, which is where the group is read from. Outputs are named sample_25000.… rather than 1000.…, which is what basename of a group path had been giving them.

  • The tick labels on a matrix output that are positions on a continuous axis rather than one-per-cell names no longer land over the first few columns of the exported TSV. They go in a # line saying what they are, and the columns keep their indices.

  • A contact map is never put on a track shelf. A hub’s type hic track has a row of its own that opens through the map door, and it was also going onto the genome row’s shelf — so open with its tracks handed it to the annotation loader, which read the binary as text, sniffed it as a BED and reported that all 32,918 of its lines failed the contig/start/end test. A true sentence about the wrong question. A shelf is annotations, a contact map is not one, and the row that opens it as a map was there all along.

  • “Open with its tracks” loads them. It opened the file and offered the tracks behind a button, which is the thing a reader who pressed those words had just pressed past. The heading verb now has a road of its own through the shell (openWithTracks) that marks the shelf as asked for, and a session with an asked-for shelf loads it instead of drawing the offer. Every other road to the same file — a row’s own open verb, a ?store= link, a dropped copy — keeps the offer, because opening a genome is not a request for eight more files. The mark is spent when it is read, so closing the tracks and reopening the same genome does not bring them all back.

  • The tracks published beside a genome are offered in the 1D browser. The map path has offered its shelf since the demo map shipped with ten annotation files — one click, and a reader who brought no tracks has a picture — and the browser path never did. Nothing had noticed, because until now the only thing that registered a shelf was a store row whose file was a contact map.

    A track hub makes it the common case and makes it visible: an assembly hub’s genome is its .2bit and its whole trackDb is the shelf that rides along, so open with its tracks on a hub’s assembly heading opened the genome and dropped every track on the way. An IGV genome row carries the same shelf and was losing it the same way. The Tracks card now offers them in a browser session exactly as it does beside a map, through the same one question and the same gate — so a restored session, which is bringing its own tracks back, is still not offered them on top.

  • A 4DN .hic drew a blank map as balanced at 1 kb and 2 kb. Juicer writes a KR entry for every chromosome whether or not the balancer converged, and where it did not the vector behind the entry is all NaN — the chicken maps carry one at 1 kb and 2 kb for every chromosome, beside a VC_SQRT and a VC that are whole. The reader chose its normalisation by name alone, per file, and never looked behind it, so every cell multiplied by NaN and the legend read 0 to 1. A chromosome whose preferred vector has no finite value in it now takes the next normalisation down the preference list (hic/HicFile.ts, normVector), and normTypeFor says which one it got.

  • Reading a single-cell container in small pieces looked hung. The listing is a thousand synchronous range requests inside one h5wasm call, so the bar said listing cells… and nothing else for as long as it took — minutes on a slow link — and the open then went on to probe one dataset header per cell for a pixel count nobody was waiting for, another four thousand round trips. The requests now count themselves out loud (listing cells · 512 reads — 8.0 MB), and the per-cell count is skipped on that source: unknown is what it is, and the File card says so.

  • A file that is not HDF5 failed as error - name not defined!. An HTML page saved under a .mcool name, a download that stopped short, a text matrix: h5wasm found no superblock and said so in its own words, which reached the screen as something went wrong. The container is now refused by its first bytes, in a sentence that names the file, and a failure h5wasm raises on a file that is HDF5 is said about that file and group rather than about nothing.

  • A contact map on Google Drive was downloaded whole. A browser tells every host it accepts gzip, and Drive takes it up on an unranged HEAD: Content-Encoding: gzip, chunked, no length. The map door read that as a host that re-encodes what it serves — GitHub Pages does, and ranges through a gzip stream are garbage — and took the fallback for such a host, one GET of the whole file into memory, silently and whatever the size: 81 MB for the .mcool this was found on, and the same road for a 2 GB one. Drive answers every ranged request raw, so a HEAD that comes back re-encoded is now followed by one more, for Range: bytes=0-, and a 206 that is not re-encoded and states its length is read by range from there — the file’s length, since the range is all of it. Pages re-encodes that too and is refused exactly as before, one request later (docs/hosting.md, docs/remote-sources.md).

  • A file picked from Google Drive took a long time to open, under a name that was its id. A Drive file is addressed by an id, so nothing in its address says what it is, and the session took the contact-map door first: a HEAD, then HDF5 hunting for a superblock at doubling offsets — a megabyte chunk at each — through the whole of a bigWig, before the track door read the file’s header three times over, four small requests each. Over the Drive API, where a request is the better part of a second, a 56 MB bigWig sat at Opening 1pq7NDWo…?alt=media… for most of a minute. The name a folder gives a file now decides the door where its address cannot, a bare address is asked what it is before the map door is tried, and the annotations worker reads a remote file in 256 KiB chunks it keeps, one handle per address — so the same open is three requests and 0.6 MB, and the loading screen and the tab say ATAC_rep2_WBcel235.bw (SPECS §24.9). A presigned URL’s tab loses its signature the same way: a query string is the request, not the name.

  • Opening a 4DN BAM as a genome fetched the whole file. 4DN’s open-data bucket exposes ETag and nothing else to a page, so the browser could not see the Content-Range the file handle reads a size off, and the handle reported nought bytes. The bounded header read was then a read of nought bytes, “not a BAM”, and the alignment file was fetched whole — gigabytes, behind Reading 4DNFIV9TCZVO.bam — to find out what its first block said. A size the handle cannot give is now asked for with a HEAD, whose Content-Length every page may read; a size nothing states is unknown rather than empty and the header is still read within its bound; and the browser opens from the header in three small requests, with the index’s download drawn as a bar on the row while it is already on screen (SPECS §8.8).

  • A merged pull request’s preview stayed in the About dialog’s version picker. Merging fires pull_request: closed — the run that deletes the preview — and push to main in the same instant, and both land in the pages concurrency group; GitHub keeps only the newest pending run in a group, so whenever something was already deploying, the removal was cancelled about a second after it queued and the review copy outlived its review. #49 and #50 were both lost that way, behind a deploy the Stores workflow had dispatched a minute earlier.

    The deploy now reconciles rather than trusting the event: before publishing, it asks GitHub about every preview in the manifest and drops the ones whose pull request is no longer open. That also covers a removal that failed outright, and one whose pull request closed while the workflow was broken. A pull request the API will not answer for is left alone — I could not ask must not read as it closed.

  • The 4DN shelf could not be generated at all. Its first live run died on IncompleteRead(43378383 bytes read, 39983032 more expected): the widened query asks the portal for every experiment set with all four of its file arrays embedded, which is an 83 MB response, and the socket closed half way through it — as a Python traceback, where every other failure in make_store.py is a sentence saying what to do. IGV and ENCODE published fine; nothing was published, because a run that loses one front end publishes none of them (the branch is replaced wholesale, so a missing store would delete last week’s good copy of that shelf).

    4DN’s search is now paged — 50 records a request, each retried on its own — and ENCODE’s is deliberately not, which the next run proved the hard way: paged uniformly, ENCODE answered &limit=500&from=0 with 403 Forbidden, having answered the same query with limit=all perfectly the week before. The two portals want opposite things of one search, both answers are measured, and --page-size is there for the run that disagrees. A 403 on a page now names --page-size 0 in its own message, since that is the fix and no status page will suggest it.

    Around that: every whole read is retried three times when a host agrees to answer and then stops talking. An HTTP status is still never retried, since 4DN answers a search matching nothing with a JSON-bodied 404 and retrying that would turn zero results into a failure. The paged walk stops on the portal’s total, on a short page, or on a page that repeats what it has already sent — and that last one fails the run when it happens early, because a portal ignoring from would otherwise publish a catalogue that draws with empty cells in nine rows out of ten.

    The suite drove the generator only through recorded responses, which is how this shipped: the recorded path reads a file and the live path walks a portal. --portal-base now points a front end at a portal served by the test itself — out of process, since spawnSync blocks the one running the assertions — and the five ways a walk ends are each made to happen, including the cut off response that started this.

  • The header’s plug and +. Connect a place and Add a file to this tab stood in the header beside the store drawer’s toggle and were the Files column’s own Open a local/remote folder and the file crumb’s Open a file said a second time, one row up; a header with three doors to a file read as three different things. Both verbs stay in the command palette, and dropping a file still adds it. The hints that pointed at “the plug in the header” now point at the Files column.

  • The Buckets card, the bucket browser and the shelves dropdown. The plug’s Buckets card listed what the Files panel already lists and edited keys the provider pane now edits; the browse bucket… dialog behind four URL boxes browsed buckets and nothing else, where the window can be connected to a folder, a Drive or a Dropbox too; and the eighteen-entry dropdown of shelves, the landing page’s URL box and its private buckets… button were the store tiles, the Load box and the plug under other names. The two 4DN maps that sat in the Demos store are on 4DN’s own page, where every 4DN map is. Earlier unreleased builds called a store a hub (metacooler.hub, ?hub=, make_hub.py, the hubs branch); none of those names is read any more.

  • The log₂ ratio combine operation. It was a scale wearing an operation’s clothes: it put the same finding in two rows and left only one of them exportable as the quantity it measured — a bigWig of log₂ ratios is a file of a picture, and anything downstream that wanted the ratio had to undo it. The reason it existed is real and is answered above, on the lane’s own axis. Two operations remain, a ratio and a difference, and either can be drawn as a logarithm.

  • The Domains (CIDs) task, which was withheld from the panel on every file and is now redundant. Everything it did is reachable and in one place: insulation calls the boundaries, calls the domains between them, and an APA over those domains is the pileup. What it added over that was a composite whose three settings never reached the step they named — AnalysisRunner.plan walks a required task at that task’s defaults — so its window, its region and its prominence changed nothing, and its estimate read a window labelled base pairs as a count of bins. A task nobody could run, whose settings did not work, duplicating two that do.

  • Diamond insulation calls the domains between its boundaries, and does it by default. A second overlay beside the calls: the span from each boundary to the next, drawn as a segmentation the way the A/B compartment track is, and exportable as BED like any other called interval set. A boundary set is half an answer — everything downstream of insulation takes an interval rather than a point, and the scaled pileup over these is the domain APA — so what every reader was doing by hand (sort per sequence, pair the neighbours) is now a tickbox, and it is ticked.

    Three decisions in it. Consecutive domains tile: one ends where the next begins, so the lane reads as a segmentation rather than as boxes with gaps, and a domain’s two corners sit on the bins the calls were actually made at, which is what a scaled pileup lines up on. Each carries the weaker of its two edges as its score, because a domain is only as well defined as its worse-supported side and reporting the stronger would let one good boundary vouch for a run of bad ones. And two boundaries make a domain, one makes nothing: the two ends of every sequence are left out rather than run to the telomere, which would assert an edge the data never placed.

    On the yeast demo: 710 boundaries across 16 sequences, and 694 domains — one fewer per sequence, which is what the arithmetic says it should be.

  • metacooler-run writes a called interval set as BED beside its TSV, the way it already wrote bedGraph and bigWig beside a profile’s. The drawer has carried a BED button since there were called intervals at all; a batch run got a TSV with a provenance header, which is readable and is not what bedtools takes.

Documentation of chromosomix v0.20.0 · built from 40ec1ec · source