chromosomix 0.10.0
What changed in chromosomix 0.10.0, released on 11 August 2026.
-
Zooming sharpens the picture. A block larger than the screen has arrived coarsened since pooling landed — a whole yeast genome at 1 kb is drawn at 3.0 kb cells, and the note under it said so honestly and told you the remedy was to open something smaller. That was true and it was not the interaction anybody has in their hands. A zoom that settles past what the block can show now reads that window again at the grid the picture could show: on the demo yeast cooler, 3.0 kb becomes 1.0 kb on one bounded read, and on a file that size the read comes out of the chunk cache without touching the network.
It changes one clause of SPECS §2 and leaves three standing, which is the whole design. There is still no tile server, nothing is cut into tiles, and no picture is ever assembled out of parts: what is drawn is always exactly one bounded block. A gesture still costs nothing — the read goes out after the window stops moving, never during it, and neither the renderer nor the panel holds a reader to issue it with. It terminates, because there is no grid below the file’s own bins: one step for a whole yeast genome, three for a human chromosome at 5 kb, and none at all for a MAG, whose block already is the finest data in the file — §2’s own case is untouched. And it is bounded by the view: a zoom can never widen past the domain the view was entered on, so it cannot become the full pixel scan §7.3 gates.
Zooming back out is served the same way and is usually free: two or three whole blocks are kept, so leaving what a fine one covers falls back to a coarse one still in hand. The colour range is measured from the widest of them and does not move while you zoom in — coarse cells are sums of bins, so a finer block holds smaller numbers, and re-measuring would re-brighten the map for somebody who changed nothing but the magnification.
It refines under an O/E map, under two files blended on the diagonal, under a 45° turn, and in every panel of a deck independently — the last of those because the navigation lock’s currency was always base pairs, so five maps at five resolutions were already in step. The one picture that does not refine is the genome × genome overview, whose axes are sequences rather than positions: a cell there is a contig, and there is nothing finer to go to.
The moment it happens is shown, not hidden. Crossing the edge of what the block can show freezes the map — no pan, no zoom — and puts a spinner in the corner of the plot until the new grid lands, so the picture is exactly where you left it when it sharpens and the change is one you can watch happen. Escape calls the read off and gives the map back, so no file can lock it. Panning to the edge of the block reads the next stretch at the same resolution: panning is a change of place, not of scale, and getting that wrong hands a reader back a four-times coarser map for dragging left.
The key in the corner now names the cell size, on its own line, and in exported figures too. It is the one thing about a refining map that the pixels cannot tell you.
Off by default, because a zoom that reads is a zoom that can wait and §2’s promise has always been that it does not. The switch is the first of the three on the map’s
picturerow — three bars, coarse at the top, fine at the bottom — beside the 45° turn and the O/E baseline, and Read finer data as you zoom in the command palette does the same thing. Off is exactly the old behaviour. The note under a pooled map now ends· zoom in to sharpenor· open a smaller block to sharpendepending on which.MULTIRES-ZOOM.mdis the study this came out of, with a closing section on where the built thing differs from the plan. -
The resolution ladder drives the zoom (§3.4, §9). “Wire the entries to the zoom control” has been in the spec since the first draft and in README’s own list of gaps as inferred but not driven. A zoom now reads from the coarsest rung of an
.mcoolor.mmetacoolwhose bins still fill a cell — and never one finer than the level you opened, which is a choice the zoom does not get to overrule: fewer stored pixels for the same picture, and — the reason that matters — a balanced map that was balanced at that resolution rather than summed, which is whatzoomify --balanceproduces and what the reader has been calling “the real answer” in a comment since pooling landed.The rungs a zoom touches are opened beside the one that is open, over the same mounted bytes, bounded at three and released with the session; a navigation still closes the map, because a navigation means the last place is finished with. Bin indices are not comparable between levels — five chromosomes of the demo
.mcoolhave one fewer 2 kb bin than the division says — so windows cross through base pairs, per contig, with a whole sequence converting to its whole self at either end. None of it is required: a plain.metacoolhas no ladder and the zoom reads the open map, which is §3.4’s own rule for what a ladder is allowed to be. -
A 4C profile can be divided by P(s). A slice of a Hi-C map is mostly P(s): contact falls by orders of magnitude with distance, so a raw 4C profile’s shape is dominated by how far away each bin is rather than by whether anything is happening there — a clean triangle with the anchor at its apex, on which the loops somebody ran the task to find are bumps visible near the anchor and invisible a megabase out. The task now offers a Normalisation choice: none, which stays the default because the raw profile is what a bigWig of a 4C experiment holds, or by P(s), which reports observed over expected at each separation.
The decay is taken from the anchor’s own sequence, accumulated from the square the task is already reading, rather than from the P(s) task’s genome-wide curve: a small chromosome divided by the average behaviour of the large ones comes out with a slope that is a property of the set rather than of the locus. Bins in trans are divided by the mean of what the anchor reaches off its own sequence, since “separation” means nothing between chromosomes.
-
Synthetic tracks survive a change of resolution. Switching an
.mcoolfrom 1 kb to 5 kb is not opening a different file — it is the same data at a different bin size — and everything a reader can see already treated it that way: the view carries across, and so does every dropped track. The one thing that did not was the output of an analysis. Run compartments genome-wide, wait the four minutes, change level to look at the same region more closely, and the Synthetic tracks card was empty: numbers the tab was still holding, dropped for no reason other than which object held them.They now come across live, as the rows the reader left — the colour, the name, the edge, the autoscale group, whether it was hidden — with the values still behind them and the exports still on them. Unrescaled, which is the honest thing and not a shortcut: a profile is one number per bin, its bins were 1 kb, and the row goes on saying so in its provenance line. Re-binning it to 5 kb would be inventing an insulation score no window ever produced. What makes that work is that a computed vector has always carried its own grid — the same reason a bigWig at one resolution draws correctly over a map at another.
Called interval sets — insulation boundaries, CID domains, A/B compartments — come across the same way, with their features and their Save as BED. A row that was already waiting for a Recompute stays waiting for one rather than being promoted to a lane with nothing in it. And this is for a change of resolution only: opening a different file still leaves the analysis behind, because a computed row is an answer about the file being closed.
-
The Chromosomes table gets the browser’s bar, and you can drag out a window. The table is where a reader of a cooler lands, and the only way out of it into a window was to type one — a row click opens a whole chromosome, and a quarter of a gigabase is a large read taken to reach a small one. The box that narrowed it asked for
chrIV:1200000-1210000, which is a coordinate somebody is usually looking for rather than one they already have.So the header is now the 1D browser’s own: the same locus box, with the same gene search behind it — by name or by accession, from whichever annotation the star picks — the same go, the same switch through to the tracks, and the same ruler. Dragging across a chromosome’s band opens that window, which is the gesture the picture was missing; a press that does not travel opens the whole sequence under it, and
*opens the whole genome as it does in the browser.The strip answers a pointer the way the browser’s own does: a hairline follows it and the cursor is a caret. A band is fifty pixels of shading with a name in the middle, so without a line there is no way to see where in it a drag would start — on a chromosome an inch wide, a megabase of ambiguity — and the caret says the gesture names a stretch rather than moving an edge. The head above it is the bar and nothing else: the title and its sentence of prose are what a view needs when the reader has to be told where they are, and the breadcrumb, the ruler and the summary line all say it better.
A band being dragged out looks exactly like one in the browser, from the same implementation: the wash, both edges as hairlines, and three chips — where the drag started, where the pointer is, and how much lies between. Those numbers are what makes a marquee aimable, and more so here than in the browser, since this ruler is the whole assembly at all times and the nearest tick can be tens of megabases from the edge being aimed at. They are this bar’s own coordinates — 0-based, clamped to the sequence the press landed in — so the chips read as what the release is about to open rather than as wherever the pointer happens to be. The canvas floats over the table to make room for them instead of pushing it down, so nothing moves when a drag begins.
Escape drops the band without opening anything, as it does on the map and in the browser, and the release that follows — the hand is still on the button — does not finish the gesture that was just abandoned.
Three things about the drag are decided rather than incidental. It clamps to the sequence the press landed in: a region names one contig, and a hand that overshoots chromosome III’s right-hand edge has still said what it meant. A movement under six pixels is a click, not a forty-base window. And what it emits is 0-based half-open with no thousands separators — what
metacooler/range.tsreads and what pastes into other tools — while the same drag in the browser writes 1-based inclusive, because that is what the browser’s own box takes.No zoom buttons and no track fitting, unlike the browser’s bar: this ruler is the whole assembly and stays that way, so a zoom control would imply a picture that can be somewhere other than where it is, and the fit buttons are about a stack of tracks that is not on screen. A
.metacool’s contig list gets the plain box it always had — a hundred thousand contigs from four thousand independently assembled organisms is a texture rather than a picture, with no band wide enough to hold a name or to aim at. -
The test suite can open a DOM. Everything here ran under Node, which is right for the arithmetic — a range query, an axis, a bin conversion, the shape of a view state — and left the other half of the application, which is cards, panels and dialogs, asserted by reading the source. A file now opts in with a
// @vitest-environment jsdomdocblock and callssetUpDom(); the ninety files that do not need a DOM go on paying nothing for one. jsdom has no 2D context and none is faked:getContextanswersnullquietly, which every figure in this app already handles because a real browser can refuse one too, and a test that wants to know what was drawn runs the figure’s owndrawinto aVectorPage— the same call the PDF export makes, and a real implementation of the drawing subset rather than a mock of one. The first three suites cover the work above: the Sequences card’s ticks against a shared selection, the feature card’s leading two lines, and where a profile goes on a file with no annotation layer.
Changed
Section titled “Changed”-
Run a task on every map in the deck. The star answers this picture; a deck of four replicates is usually open because they are being compared, and the comparison is four insulation tracks rather than one. run on all N maps sits beside Run whenever there is more than one map, and is hidden when there is not — a control offering to do something to a set of one is a control a reader has to work out.
One job per map, queued rather than started together: the worker mounts one file at a time, and five h5wasm instances with five dense blocks live at once is the moment a tab can least afford it. Each result is filed against the map it was measured from, so a batch leaves a row per panel rather than five rows on the lead. The panel row says which map and how far through — the percentage is one map’s progress, and a reader watching it reach 100 four times with no idea there are five would read the fifth as a stall. Stop stops the batch where it has reached: the maps already measured keep their results, the rest are never sent.
A job now carries the file it runs against rather than asking the session at dispatch, which is what makes any of this possible — and it retires a failure the old shape had to invent, the open file changed while this job was queued, since there is no longer a moving answer for a job to disagree with.
-
A star says which map an analysis runs against. A deck of four replicates is usually opened to measure one of them, and until now the Analysis column could only ever read the session’s own file — so the answer was to close the deck, open the replicate as the session, run the task, and put the deck back. Every panel now carries a star, exactly one is filled, and the filled one is the map every task is priced against, framed by, run on, and files its results into.
It is not the lead, and that is the whole design. The lead is what the session is of — the breadcrumb, the tables, the tracks, the second-file split all name it — and it does not move (§16.9): making a star change it would rewrite half the furniture to run a P(s), and a region valid in one assembly is not a place in the next. So the star is a second, smaller idea. It starts on the lead, it goes back to the lead the moment the map it named leaves or the deck closes, and it is remembered per file like the deck’s own column count.
A starred companion reads its
/contigsand/genomeswhen the star lands on it and not before: those two tables are what a task needs and a picture does not, so an eight-panel deck fetches sixteen of them only if a reader stars eight maps in turn. And what a task produces goes to the map it was measured from — an insulation track computed from the third replicate is a track about the third replicate, and drawing it beside a map whose numbers it is not is the one way this could have been quietly wrong. -
A deck grows as far as the machine will carry it. The limit was six, and six was a judgement about how many maps are useful rather than about what the tab can do — the wrong kind of limit on a grid a reader draws. A row of eight replicates, a 4 × 4 of a community’s MAGs, a column down one chromosome: each is a legitimate figure and none of them fits in six. The real limit was never that number. Every panel’s share of this tab’s memory is already this tab’s budget divided by the panel count, so the reads get smaller as the deck grows and refuse when a share will not do — a limit in bytes on the machine in front of the reader, which is the only honest one. What is left is a backstop at 64, an 8 × 8 grid, so that a hand-edited hash asking for ten thousand cells is refused before ten thousand workers are started.
-
Closing a deck no longer deletes the tracks that were on it. A deck put down is a statement about layout and never about what the reader has imported, and the deletion was uneven in a way that read as a bug: a track they had also put on the lead survived, and one they had only ever put on a companion did not — so opening four replicates, giving each its own bigWig and closing the deck lost three imports to a gesture about how many pictures are on screen. The lanes on the departing maps still go with them; the tracks stay on the Annotations panel, drawn nowhere, which is a state the model has always had and is one tick from being on the lead.
-
Manual marks can be placed on any map in a deck, not only on the lead. They were always drawn on all of them — the marks live in one store every panel reads from — and only the gesture was the session’s own panel, which made the feature look broken in the layout it is most useful in: a domain boundary drawn across four replicates is the thing a deck is for. Every map on screen is armed now, as one mode rather than five: whichever picture the reader places on, whichever one they press Escape over, and the card putting its button back all end it everywhere. Armed on five panels and disarmed on one would leave four maps swallowing clicks the reader meant as navigation.
-
Escape puts the analysis drawer down. It was the one overlay in the application that did not answer the key, on the argument that a modal’s Escape destroys what the reader is reading — true of a modal, and not of this. Closing the drawer is the ✕, and the ✕ has always left everything running: the run continues, the panel row goes on reporting it, and re-opening the task brings back the form and the figures. Two things still outrank it, which is what keeps it a dismissal rather than a mode: an event something else has already handled (a marquee on the map, a caret in the legend, a search box’s hit list), and a caret in one of the drawer’s own fields — a half-typed anchor abandoned with Escape means that box, not the panel around it.
-
The map card reads outward from the axis. Coverage and the marginals — what every bin has — then P(s), how contact falls away from it; then 4C, one locus’s slice of that decay; then insulation, the domains it resolves into; then the compartments those domains sit in. Each is a question the one before it makes it sensible to ask, which the old order (P(s), coverage, insulation, 4C, … compartments) did not read as.
-
The Aesthetics panel is marked by a plot rather than by a chevron. Its two siblings are marked by what they hold —
layersfor the annotation stack, a beaker for what can be computed — while this one carried a left-pointing panel arrow that said only this folds, which all three do and which the arrow’s direction already showed. -
A hand over a band on the Chromosomes bar. The strip carries two gestures and the cursor is what tells them apart before either happens: a press picks the sequence under it, so the pointer is a hand there, and the caret comes back the moment the hand starts travelling, because naming a window is what selecting text does.
-
A deck’s two shared settings are presses, not switches, and the first map’s resolution takes the deck with it.
Changing the first map’s resolution — from the breadcrumb’s own picker, from a link, or from the deck’s bar — now moves every companion to that resolution. A reader who changes the rung means the deck rather than one cell of it: the whole claim a deck makes is that its pictures are of one place and can be read against each other, and half of them at 1 kb beside the rest at 10 kb is a claim it is no longer making.
A file that has not got that level stays exactly where it is and says so under its own map, naming what it does have. The nearest rung would have kept every panel doing something, which is the wrong kind of helpful: it answers “put these on one resolution” with a deck at two and says nothing about it.
fix all scales and fix all resolutions are now bordered action buttons rather than switches, first in the bar, ahead of the chain. A switch here made a promise it could not keep — left on, every later nudge of any panel’s clip either had to be undone or left the deck claiming a shared scale it no longer had. A press says something that stays true: these are all on that one’s numbers now, and the very next nudge is honestly the reader’s own again. Both read from the starred map, which is the map every other “all of them” in this app is measured from, and both move the first map too when the star is elsewhere. Maps that could not follow a resolution are named in the deck’s own line.
The switch these replace read
one scale/own scales— two phrases of the same length in the same shade, so the only way to read the setting was to know the phrasing, and the only way to learn what pressing it did was to press it. -
Run says which map it will measure. Beside one map
runis a complete sentence; beside five it is dangerously incomplete — the reader has one map starred and four not, the difference between them is a small filled star in each heading, and a button saying onlyrungives them nothing to check that against before spending a minute of compute. On a deck it now reads run on ★, carrying the deck’s own mark rather than the file’s name: a name is longer than the button and would resize it every time the star moved, and the mark is what the reader is already reading the deck by. The name goes in the tooltip. -
The adaptive-resolution switch is hidden for now. The preference itself is untouched — global, honoured by every panel, and still reachable — it is simply not in the map’s control row.
-
The turn control is a swap, not a light. Its mark is the picture the next press produces: a square map offers a triangle, a turned one offers a square. Lighting it as well said both things at once — a turned map showed a square in accent blue, which reads as “square is on” beside a band along a diagonal. The neighbouring toggles keep one mark and change their state, so the tint is the only thing that can carry it there; here the mark already does.
-
run on all mapsis drawn as the button it is. It sat besiderunin the quiet, borderless form, which reads as an afterthought or a link rather than as the other half of a choice. -
Dropping a contact map adds it to the deck instead of replacing what is open. It was the one route in this app where a drop destroyed something: a track dropped on a map is added to it, a
chrom.sizesdropped on a browser replaces the lengths and keeps everything else, and a cooler dropped on a map threw away the file, the tracks placed on it, the window and the colour scale — for a gesture that everywhere else means and this too. Now it opens beside what is there, which is what the+edges and the deck’s URL box already did one file at a time, so the three ways of reaching a second map agree.Several at once populate a whole deck, which is what selecting four replicates and dropping them meant. The landing page took the second map as a blend — the lower-left triangle of one square — and discarded the third onwards without a word; both halves of that were wrong. The blend is the strongest claim two files can make and is gated accordingly (one grid, so one bin size, so one chromosome list), so the commonest two-cooler drop met a refusal where a deck would simply have worked, and a gesture that loses two of four files reads as the application dropping them. The blend is still one click away on the compare card, which is where a claim that strong belongs.
Where there is nothing open to put them beside, the first map still opens the session — that is an empty seat being filled, not a replacement — and the rest become its companions. Replacing the open file is now the deliberate gesture it should always have been: the picker,
Open file…, or a new tab. -
Two turned maps in a deck cut their bands to different depths. A band reaches as far from the diagonal as its own box has rows for (§17.2), which is the right rule for one map and cannot work for several: a grid lays its cells out over more than one frame, so two panels asking their own boxes at their own moments get two answers and neither is wrong — and the depth is only ever grown, so nothing brought them back together. Pressing
+beside a turned map halved the first map’s width, which asked for a shallower band that it refused to shrink to, while the companion arriving beside it cut a fresh one from the narrower cell. The result was two visibly different bands, the lock refusing to move them together, and a refusal blaming the two files’ resolutions for what was a layout race — a claim about their data, made out of a timing accident, and curable only by turning the deck off and on again. The deck now shares the distance, in base pairs, exactly as its navigation lock shares the window and for the same reason: each panel converts through its own bin size, so panels of unequal resolution reach the same place rather than the same number of bins. -
An empty cell of a deck was drawn square whatever it stood next to. A contact map is square and a turned one is not — it is a band along its diagonal, a fraction of the height of the square it was cut from — so pressing
+beside a turned map produced a card several times taller than the picture it was offering to stand beside. A grid row is as tall as its tallest cell, so that card set the height of the row and every map in it was pushed to the foot of a rectangle of air. The deck already measures the lead’s plot and pushes the number down rather than letting each panel derive one; it now measures both sides of it, and the empty card is the size of the maps beside it whatever shape they are. -
Half of this application is cards, panels and dialogs, and it is tested now. The jsdom scaffolding landed with the Sequences card; what it was reaching was seven files.
VirtualTable— which is under both tables, and so under where a reader of a metagenome spends most of their time — had none at all, and every one of its behaviours was asserted by reading it. Twelve more DOM files cover the table (filters, the sort cycle, the header box that speaks for what the filters left rather than for the file, keyboard walking, what virtualisation actually builds, what it remembers, and what it must forget), the genomes table’s two structural spec rules, the one search box four views share, the modal and the context menu and every way each of them closes, the small shared controls withui/dom.tsunderneath them, the ruler’s clip, the analysis parameter memory end to end, the breadcrumb, the error view, the info panel’s §7.4 statistic and the two controls a colour scale is set from. Five of the fixes above were found by writing them. -
Dead code taken out. Nine exported symbols with no caller anywhere in the repository, plus
VirtualTable.setRows, which was a public method nothing called and which cleared the selection without telling the toolbar that reads it.magBalanceis the one worth naming: it says “params helper shared by the runners” and the runners have always read the parameter themselves.mergeParamswent with the drawer’s own parameter scope, which is what it existed to serve. Out withsetRowsgoes the sort cache’ssourcefield, which existed only to be invalidated by it. -
Non-ASCII names are not supported, and are no longer tested as though they were.
/contigsis cooler’s/chroms, and cooler decodes every column of it with an ASCII.astype("U"), so metacooler refuses to write a non-ASCII/contigsor/genomesname at all —check_ascii_namesbefore the first write andschema.validateafter the last. Two fixtures here had exactly that shape, and the cost of keeping them had already come due:make_fixtures.pycould no longer build them, sonpm run fixturesstopped after two of eleven files and the fullnpm testwas unrunnable from a fresh clone. One of the two also had no Python oracle at all — the reference implementation cannot open it — and the partial golden the whole toolchain carried existed for that one file: acoveragefield, agolden: 'partial'branch inmake_golden.py, apython_openfailure record, afullflag on every golden map and afull: truefilter at three call sites. Fixtures, goldens, manifest rows, the suites that read them and that machinery are all gone; the generator now lets a fixture Python cannot open raise, because that is a broken fixture rather than a documented limitation.What is kept is the decode. An extra
/genomescolumn may still hold an accented taxonomy — legal metadata cooler never sees — sodecodeFixedStringsis now tested directly, over bytes, for the three things that go wrong: a multi-byte character read back as itself rather than as mojibake, NUL padding stripped, and a record sized in bytes rather than in characters. No fixture is needed to hold them, which is the point. -
The goldens were built from fixtures that are no longer there. Eight of the eleven had moved —
npm run fixturesrebuilds both halves, and the fixtures a current metacooler writes are not byte-identical to the ones the committed oracles were made from. Almost everything still matched, which is what made it dangerous: what did not was whatever the two versions happened to disagree about — a root attribute stored asint32in one andint64in the other — and it surfaced as one deep object comparison failing in the middle of an otherwise green suite, which reads as a bug in the reader and is not one. The goldens are regenerated, so the two halves are one set; the only substantive change in them isfloatcount.metacool’sbin-size, now recorded as theint64the file actually holds. And the skew is now checked before any comparison is made: the manifest already records the byte size of every fixture as it went in, so a fixture that has moved is named, with the command that fixes it. -
Three docblocks in
metacooler/aggregate.tshad drifted off their functions. The paragraphs describingaggregateMatrixandsparseAggregatorwere stacked abovecoarsenGrouping, which has its own; the two exported functions they belong to had none where they are. Moved, unchanged. -
The pooled read’s own test asserted an invariant the rule does not have.
pooling.test.tsrequiredcontigsPerCell === 1for every whole-file pooled read, which was true of the old two-regime rule and is not true of the capacity rule that replaced it: a sequence shorter than a cell shares one with the neighbours it is beside, soladder.mmetacool::entries/4000— which holds two single-bin contigs of one genome side by side — reports two at a stride of two, correctly. The assertion is now that the read used the groupingpoolBinsbuilt, which is what it actually owes the caller.npm testwas red onmainbecause of it; the cell-for-cell comparison against the dense oracle beside it was passing throughout. -
The analysis panel stopped asking questions it can answer itself. A parameter is a question the reader is entitled to a different answer to; everything with one right answer per file belongs to the file. Going through the catalogue on that rule took a control off nearly every task.
Balancing is the case that settles it. “Use balancing weights” was a tickbox on six tasks, off by default on five of them, and it is the one setting in the panel where the wrong answer produces a figure that looks entirely reasonable — an unbalanced P(s), insulation track or 4C profile is a plausible shape, and nothing in the picture says which one was asked for. A reader who opened a balanced file answered the question by opening it. It is now derived from the file’s weights and shown nowhere, on P(s), diamond insulation, 4C, compartments and APA. It is still a parameter in every other sense, so the runners are unchanged and an exported TSV still says what the numbers were computed with — and on the four MAG tasks too, which share the panel with them and would otherwise be one tickbox among nine rows that no longer have one. Coverage keeps its tickbox, because there it is a diagnostic rather than a choice: a balanced file should have a flat total, so one that does not is a file whose weights do not fit it.
Distances are asked for in bins. P(s)’s minimum separation and diamond insulation’s window and call spacing were typed in base pairs and immediately divided by the bin size. The near-diagonal is a fact about the grid rather than about the genome, and the same typed number meant three bins at one resolution and one at the next — so they are counts now, and the diamond’s default (10 bins) is exactly what
10 × binsizeused to resolve to.A whole-sequence picker starts on every sequence. It was seeded from the map’s framing, which silently narrowed P(s), insulation and compartments to whichever chromosome happened to be on screen and then reported the result as though the reader had chosen it. Empty is an answer for those controls — it is the entry the list itself offers first — so it is left alone. A locus box is the opposite case and still starts where the map is: 4C’s anchor is a place.
P(s) draws every sequence. The Sequences to draw box kept the largest N and said how many it had dropped, which turned “how does contact fall with distance, on each of my sequences” into a question about a number nobody had a reason to pick. All the curves are computed and all are drawn; what is capped is the legend, at three lines with a
+N more, by the drawer that has to fit it.Compartments takes a set of sequences rather than refusing until one is named — see the entry below. Its Quality section is gone: the balancing tickbox is derived, and the minimum row coverage is a threshold below which a row is noise rather than a knob that changes an answer.
Coverage starts with the total marginal alone, which is what the task is opened for; the cis fraction is a second lane arriving unasked. Track correlation bins at 100 bp rather than 10 kb — 10 kb windows smear a promoter-width peak into its neighbourhood before the arithmetic starts — and no longer prints “1 bin at 10.0 kb” beside the box, a caption measured against a contact map this task does not read and need not have open.
Domains (CIDs) is withheld from the panel. Everything it does is reachable: diamond insulation calls the same boundaries from the same diamond and exports them as a BED, and the pileup over those calls is what APA does over any interval set. The descriptor is left whole, so bringing the row back is one line.
-
Compartments correlates several chromosomes at once, cis and trans. It took one sequence, and before that it refused until one was named, on the argument that compartments are a property of one chromosome. That argument is about the expectation — which really is per chromosome and stays per chromosome — and it does not carry to the correlation: two chromosomes in the same compartment touch each other in trans, and a square holding those trans blocks is where a genome-wide eigenvector’s signal actually comes from. So the control is now a list of tick boxes with All and None beside it, every sequence is ticked to begin with, and what comes back is one E1 over all of them rather than one per chromosome. Ticking a single sequence gives the per-chromosome answer as the degenerate case of the same arithmetic.
The two denominators are the whole of the difference and are kept apart: a cis cell is divided by its own sequence’s decay at that separation — never a pooled curve, which would divide a small chromosome by the average behaviour of the large ones and read as a gradient along the eigenvector — while a trans cell has no separation at all, so it is divided by the mean of its own pair’s block, per pair rather than one global trans mean.
And which halves it reads is a control: cis only — the default — cis and trans, or trans only. These are three computations rather than one with a filter over it. Cis only changes the shape of the run: one correlation per sequence rather than one across them, because a single square with its trans blocks removed correlates to a block-diagonal matrix whose leading eigenvector lives entirely on one chromosome and is zero on the rest — a genome-wide cis run done that way returns one chromosome’s compartments and fifteen flat lines, which is wrong in a way that looks like data. It is the default because it is what people mean by “the compartment track”, and because it is far the cheapest of the three: the correlation is cubic, so sixteen squares of 1,500 bins is a fraction of one of 24,000. It is priced against the largest sequence and gated per sequence, which makes it the one mode that still runs at a resolution the other two are refused at — and their refusal says so. Trans only is the other standard call: the cis blocks carry a near-diagonal that dominates every row, and leaving them out makes the eigenvector a statement about which sequences share a nuclear compartment and nothing else. It needs two sequences by definition and says so.
What it reads is the coalesced runs of what was ticked, so everything ticked is a single read of the whole file rather than 136 rectangles, and a scattered subset pays a rectangle per pair of runs instead of reading the gaps between them. The bin-count gate now counts the whole ticked set — a yeast genome is 12,100 bins at 1 kb and 1,210 at 10 kb — and the refusal says to untick some or open the file coarser. The correlation is cubic in the bin count and now says so beside the megabytes, reports progress from inside its own loop, and can be cancelled there.
-
ignore_diags, on compartments and on insulation. cooltools’ setting, spelled the same way and meaning the same thing:0counts every cell,1drops the main diagonal,2drops it and the two beside it —|col − row| < n— and 2 is the default, which is cooltools’ default and what the numbers a reader compares against were computed with.It is not a tidiness knob. The near-diagonal is where self-ligation and unligated fragments end up; those cells are the largest in the matrix and the least structural. In a compartment run a row’s correlation with its neighbours is carried disproportionately by the two or three cells nearest the diagonal, which say only that the bins are adjacent — and every chromosome’s rows agree about that, so it enters the eigenvector as a component that is really a picture of the diagonal. In an insulation run the diamond never touches the main diagonal, so what the setting trims is its inner corner; at a small window that corner can be most of the sum, and a track carried by it is a picture of how deep the near-diagonal is rather than of what crosses the bin. The diamond’s coverage threshold is measured against the cells it is allowed to count, so raising the cut does not quietly raise the bar a bin has to clear.
Trans cells have no separation for a diagonal to be measured from, so they are never affected — and the control is hidden under trans only, where there is nothing for it to do.
-
A/B compartments come out as segments, not only as an eigenvector. E1 is a number per bin and the thing that gets cited is the interval — this arm is A, that half-megabase is B — and getting from one to the other meant exporting the bigWig and thresholding it in another tool. It is one comparison against zero, and this app can make it without the ambiguity a downstream threshold introduces, because it knows which bins were excluded and does not have to guess whether a gap is a zero. So a run now produces an overlay of A and B runs beside the track, with the mean of E1 over each run as its score, exported as a BED like every other called interval set. A gap in the usable bins ends a run rather than being bridged: a segment drawn across bins that took no part in the calculation would be asserting a compartment where nothing was measured.
-
Pick on map is one gesture. The 4C anchor was picked with a Shift-drag, which is a held modifier on top of a mode that already has a lit button and a changed cursor — asking twice. While it is armed an ordinary drag names the band, a tap names the bin under the pointer, and neither zooms, pans, nor navigates: the only thing the gesture does is answer the question the form asked. It still disarms after one pick and on Escape.
-
A saddle is a square. The two cell sizes of a matrix figure were computed independently, each filling its own axis, so the aspect of a saddle or a correlation heatmap was decided by how wide the drawer had been dragged. That is a picture whose shape carries no information and reads as one whose shape does — a saddle is read by comparing its two corners, and a corner stretched 40% along one axis looks stronger than the one opposite it. Cells are square now, so an
n × nmatrix comes out square in any panel and a non-square one keeps its own aspect exactly. -
The Sequences card is how a cooler’s map picks its chromosomes. The card the 1D browser has always had — every sequence, its length, a bar for the shape of the assembly and a tick box beside each — is now in the General column whenever a cooler is open, and beside a map the ticks are the picture: none is the whole genome, one is that chromosome’s block, several are those chromosomes against each other. It replaces the Chromosome card, which sat in the same place describing the one sequence the breadcrumb already named, from a table the reader can open in full, and offered no way to change it.
The binding runs both ways, which is the whole of the design. A tick navigates; a navigation re-ticks — so the breadcrumb, the Chromosomes table, the back button and a pasted link all move the boxes with the picture, and the card can never say “showing 1 of 16” over a whole-genome map. A table view is the one thing that leaves the ticks alone rather than clearing them: nothing is drawn there to disagree with, and clearing would throw away a selection being made on the way to the map. Ticking is also order-free — the axis comes out in the file’s order however the boxes were clicked, rather than in the order they were clicked in.
The Chromosomes table is untouched, and it is still where columns are sorted, filtered and read; what moved is only the picking. A
.metacoolkeeps its Contig card and its genomes table: there the unit is the genome, and a picker with a hundred thousand tick boxes in it is not a picker. -
The virtual 4C lane is gone, and Shift-drag is a zoom. Shift-dragging on a map used to conjure a strip beside it summing that band of the matrix. Every part of that the 4C task now does better: it reads the pixel table rather than whatever block happened to be loaded, it produces a track with a row on a card and a bigWig to export, and it carries a provenance line saying what it is. A dimmer second copy of the same answer, appearing without being asked for and redrawing itself on every pan, is worth removing even when it works — and the two disagreed at every level but the finest.
What replaces it is the gesture the picture was missing. Shift-drag names a band of one axis and zooms to it, on the map, on the track lanes beside it, and in the 1D browser — one conversion from pixels to bins behind all three, so a stretch dragged out next to the map is the stretch the map goes to. The axis follows the hand, so a drag a few degrees off vertical still means what it plainly meant, and the axis nobody dragged keeps its centre.
And every one of these gestures copies what it framed. Shift-drag writes
chrIV:500001-510000: no thousands separators, a plain hyphen, 1-based inclusive — the difference between a string that pastes into samtools, bedtools, IGV and this app’s own locus boxes and one that only looks right. Option-drag, which selects a box, writes both axes aschrI:1-10000|chrII:40001-50000, rows then columns, each half exactly what that stretch would produce on its own.Nothing else about lanes changed, because the lane was never the point: the
viewpointkind and card are gone from the store, and the four edges, the drag-to-another-edge, the tooltip and the exported figure are the code they already were. -
A shared link carries the two new readings, because both are part of what is being looked at rather than how it is shaded:
oefor the baseline andopfor the comparison. The normalisation needed no key of its own — it is ascore, and resolves through the same list the picker is built from, so a hash naming one this build cannot serve lands on counts rather than on a read that cannot be answered. -
Adding or closing a map no longer rebuilds the one beside it.
mapsnames what is on screen beside this view, and the deck reconciles itself from it before the render path decides anything — so the lead has nothing to be rebuilt for. Left out of that list, every press of+cost a worker round trip and threw away the reader’s pan and zoom for a change that happened in another panel; drawing a 2 × 2 deck paid it four times, and on a gated whole-genome map (§7.3) each one would have put the pixel scan back behind its button. -
The turn and O/E are toggles, side by side, in one row of the Map card. They are the two controls that change what picture this is — one the geometry, one the units — as against the score and the clip, which change what a number is worth, and the overlays, which change what is drawn over it. They were an icon button that looked identical before and after it had been pressed, and a tickbox beside it that made the same kind of setting look like a different kind of thing. Both are now lit the way every overlay toggle is. The turn’s mark is the shape it will give you — a triangle while the map is square, a square once it is turned — rather than a mark for the state it is in, which is the one reading that needs a second glance at the picture to interpret. O/E’s mark is the two letters, because a ratio has no shape and the nearest drawn thing is a division sign that would say “divide” without saying by what.
-
No drawn pointer where a guide line is already pointing. The crosshair is the better instrument of the two — it is drawn at the cell the app has actually resolved, it runs the width and height of the picture, and it continues into every track — while the arrowhead beside it is a second, blunter mark for the same position, sitting on top of the pixel it names. So it goes, exactly where the line comes: on the map while the crosshair is on and the pointer is over a cell, in the 1D browser while it is on and the pointer is on the axis rather than in the name column, and on a lane stack always, since a lane answers a pointer with a hairline whatever the preference says. The transient cursors stay — the closed hand while panning, the crosshair while a selection is about to be drawn — because those say what is happening, which no guide line does.
-
The deck’s bar reads from the left, with the one control that ends the deck at the far right. The spacer used to sit second, which split a row of one kind of thing into a chain on the left and three controls floated against the right-hand edge — on a wide window, a screen apart, so reading the bar meant looking in two places and which half a control was in said nothing about what it did.
-
The README no longer says balancing is unavailable. It has been built since 0.2 —
/bins/weight, two bounded slices for the bins on screen,raw · wᵢ · wⱼ, and a control that became the score picker in 0.9 — and the paragraph claiming otherwise had outlived the divergence it described by seven releases. Since every analysis task wants balanced input, a stale line saying it cannot be had was one misreading away from someone concluding these numbers are computed on raw counts. The layering rule above it is corrected in the same way: no coordinate ever comes from/bins, which is what §3.3 says and whatbins-guard.test.tsproves, and the weight column is the one bounded exception. -
“No write access” now says what it always meant. The viewer has produced files since 0.2 — PNG and PDF, then a saved session, then a BED of hand-drawn marks in 0.5 — and a bigWig is one more of them: new bytes handed to the browser as a download, needing no server, touching nothing that was opened. The non-feature is editing an input, never authoring an output, and SPECS §13 says so now rather than leaving a reader to reconcile the two. Which is also why there is still no writing of
.cool,.mcoolor.metacool— h5wasm could create one, so that is a decision and not a limitation.
-
fix all scalescarried the ramp across the deck and left every range where it was. A panel has two colour scales and the press read the wrong one:scale()reports what the reader has set, andNaNon a bound is how it spells nobody has — which is the state of every map whose clip has never been nudged, and so of nearly every map. The transform and the colormap travelled, the twoNaNs were dropped on the way out, and each panel kept its own auto range, which made the button look inert. It now reads what the rasteriser resolved — §7.1’s auto rule, run once per read, and the same pair of numbers printed under the map’s own key — so pressing it pins every other map to the numbers the starred one is actually drawing with. -
A batch measured every map from the first map’s pixels, on the one chromosome that was in view. A task that can run off the block already on screen is handed it rather than reading the file again, and whether that block still describes the job is checked by comparing extents and a bin size — which, in a deck, is exactly what every panel has in common. So “run on all maps” lent one panel’s pixels to all five jobs and five maps came back with one map’s answer.
Only for the region that happened to be in view, which is the worst shape a bug of this kind can have: everywhere else there is no block to lend, so every other chromosome was read per file and correct, and the numbers agreed with themselves everywhere a reader would think to check. The block is now lent only to the job about the map it is a picture of — the starred one — and every other job in the batch reads its own file, which is what the worker’s second mount has always been for.
-
A companion drew none of the tracks placed on it, and a reopen collected them all onto the first map. Two faults with one shape, and between them they made a deck’s annotations look like they had never worked.
The session builds a new
AnnotationStoreon every open — a change of resolution, a re-drop of the file, a link into another entry — and the lead’s view is rebuilt around it while a companion’s is not. So from the first resolution change a companion was subscribed to a store nobody writes to any more: every track placed on it afterwards, computed or dropped, went into the live store and was drawn nowhere. A companion’s map is now rebuilt when the store under it is replaced, which costs a construction and no read — the block is already in hand.And carrying a track across that reopen carried the import without its lanes.
AnnotationStore.beginplaces everything it is handed on the lead, which is what importing means and is exactly wrong for a row that is coming back, so a deck of four maps each with its own bigWig came back with all four stacked on the first one — and a track that had only ever been on a companion arrived on a map it had never been on. The lanes are snapshotted with the rows and reconciled back onto them, the lead’s placement included or removed as the snapshot says. -
A batch over several maps produced one track, not several. The id a computed track gets is derived from the output so that a second run replaces its own numbers instead of stacking a lane beside them — and it named the output alone, with no mention of the map. So “run on all maps” over five replicates made five coverage tracks under one id: each one replaced the row before it, and each placement moved the single surviving lane onto the next panel and off the last. What came back was one track, on the fifth map, and four maps with nothing drawn on them, which reads as the batch having failed rather than as five results landing on top of each other. Ids are now scoped to the map, and a row from a companion carries that map’s name in its label so five rows are not five identical
coverages. The first map keeps the bare id it has always had — a restored session and everyRecomputeon a row written before decks existed resolve through it. -
A companion’s band was drawn forty pixels tall beside a first map four hundred tall. A turned map’s box is a pair of numbers fitted per panel against how much room that panel measures itself to have, and a companion is built already turned — so the measurement happens before its plot has been laid out,
max-heightresolves to nothing, and the fit bottoms out on its own minimum. A square map never showed it, because a square’s height follows its width throughaspect-ratioand the width was already being pushed down. -
How far through a batch a task is has its own pill, beside the percentage rather than in front of the sentence under it. The percentage is one map’s progress and reaches 100 once per map; a reader watching the fourth of five fill and reset had nothing to tell a batch from a stall.
-
A map added to a deck opened at its own zoom rather than at the deck’s. The navigation lock learns where the deck is looking from a panel reporting a window, and a panel reports when it draws. The first map of a deck was drawn long before there was a deck — nothing was listening while the reader zoomed into a locus and went looking for a second file — so at the moment the deck came into existence the lock knew nothing, and the companion that arrived was caught up to nothing: a whole contig beside a lead showing 40 kb of it, under a bar saying the two are one navigation. It usually looked right, which is why it survived: re-parenting the first map into the grid resizes it, a resize is a redraw and a redraw is a report, so the window generally arrived a frame after the deck was built and a frame before the companion’s bytes came off the network. A deck that lines up because the network is slower than a relayout is not a deck that lines up. The deck now seeds the lock from the first map’s present window whenever it is about to catch a panel up to it — on a companion settling, on a panel being put back on the lead, and on the first map itself being put back in step. Seeding moves nobody and never overwrites a window a panel has actually reported, so a reader’s own choice still outranks it, and it records no debt, so the next real gesture still drives the deck. The same gap opened after every navigation, where the lock is cleared on purpose and a small companion could finish reading before the first map had drawn the new block.
-
The star could be left on a map that was not in the deck. It is remembered per file, and a companion’s internal id comes from a counter that climbs for the life of the tab — so a reader who starred a companion, closed the deck and opened another one restored an id this deck would never mint. It was reconciled only where a slot departed: a link naming fewer maps, a cell closed, the deck closed. A deck built by “add a map” reaches none of those, so the star sat on a panel that had never existed — no star filled anywhere, the first map’s own star offering to make it the target it already was, and every task’s result adopted onto a map with no panel to draw it. The star is now reconciled wherever the list of cells changes, arrivals included.
-
A track placed on a companion outlived the companion.
keepMapssays of itself that it is reconciled against the deck rather than driven by an event, because “doing it once per render cannot go stale” — and it was reached from exactly one place, the deck’s own close. So closing one map out of four, or following a link that named fewer maps than the deck held, left that map’s lanes in the store bound to an id nothing would mint again: invisible, because the placement control lists the maps that exist and a lane on one that does not has no row to be unticked from, and still counted by everything that asks a track how many maps it is on. It now runs wherever the grid is repainted, and it is keyed on the cells rather than on the pictures in them, so a companion that is still reading — or one whose current view is a table, and therefore momentarily has no map — keeps its placements. -
A local companion was offered by its internal id. The list of maps a track can be drawn on fell back to the deck’s own name for a slot for anything that was not a remote file, which is every file a reader drags in: the placement control offered
p3as the map to draw a bigWig on, in a list where every other row said what it was. It now names every companion by the URI that panel is actually reading, which also fixes the neighbouring case — a map that descended into an entry of a collection is listed asrep2.mcool::1000, the rung on screen, rather than as the collection it was dropped as. -
Changing a starred companion’s resolution did not change what a task read. The facts an analysis needs are fetched once and held, which is right for a panel whose address never moves and wrong for the one control that moves it: picking
::5000on the starred map ran the next task against::1000and captioned the result with the rung on screen. The cache is dropped with the other two the entry picker invalidates, and the deck re-reads it where the panel it happened to is the starred one. -
Smaller things in the deck. A companion’s star named the file it was dropped as rather than the entry it descended into. A deck of one map — the state a reader meets first, because “add a map” builds the deck and shows its bar before a file has been chosen — said “1 maps”. A map dropped on a cell index out of range repainted the grid and rewrote the hash with the list it already had, behind an identity check that could never fire.
-
The ruler drew into a box shorter than the one it had been given. Both callers size their canvas with
rulerHeightFor(scale)— the sequence band is a fixed target and the coordinate row under it is a line of text that has to grow with the text — anddrawRulerclipped toRULER_HEIGHT, which is that height at a scale of one. Nothing runs at a scale of one: the default is 1.35, for the reasonui/textScale.tsgives, so the bottom of every tick label in the 1D browser and on the Chromosomes table’s bar was shaved off for everyone who had never touched the control, and further for everyone who had turned it up. The height travels with the frame now, and the clip is the strip the table was actually pushed down by. -
A 4C profile with trans, divided by P(s), was two quantities on one axis. The trans half was divided by the mean of what the anchor reaches off its own sequence, and the cis half was left as raw counts — on the stated grounds that the decay had already been divided out of it, which is true on the cis-only path and was never true on this one, where nothing had read a square at all. So a track labelled
O/Eheld ratios near 1 across fifteen chromosomes and a band sum in the thousands on the sixteenth, which reads as the strongest signal in the file and is a unit error. The anchor’s own sequence is now normalised by its own decay on both paths — one on-diagonal square against a pass that is already touching half the pixels in the file, and only when the ratio was asked for. The curve is also accumulated throughExpectedAccumulator.addBlockrather than through a loop written beside it, which is the one that files a cell under the separation its absolute bins imply and cannot read a block down the wrong stride. The square is read fromcountrather than borrowed from the view’s block, whichusableBlockvouches for the resolution and the balancing of and not for the column: a map drawn fromexpectedwould otherwise have put the two quantities back in one track by a second route. -
An exported session carried every task’s parameters and put none of them back.
analysis/memory.tskeeps them in one scope,analysis:params, and its docstring says exactly why:SessionConfiggathers a session by reading a fixed list of scopes, which is what makes an exported config provably the same thing a reload would restore, and a scope named after a task id cannot be on that list. The Analysis drawer had grown one per task anyway — it read and wroteanalysis:<task id>while the workspace wroteanalysis:paramson Run — so the two halves of §18.2 were persisting to different places and the config’sanalysisblock was written, exported, imported and never read. One scope now, through the one module that owns it; the drawer keeps its own key-by-key validation, which is the part that knows what shape a parameter is meant to be. -
Every dialog opened with the caret on its close button. A selector list answers in document order and the ✕ is in the header, so “the first focusable in the panel” was always Close: Enter dismissed the dialog instead of submitting it, and reaching the box you opened it for meant tabbing past the one control that throws the work away. The caret goes to the body’s first enabled control, and to the panel’s only if the body has none.
-
A numeric table filter refused a negative exponent. The pattern had no
-past the first character, so1e-5did not parse and>1e-5compiled to nothing — which leaves the column unfiltered rather than saying anything, on exactly the column (a density) a reader writes that way. -
Cancelling a task did nothing on exactly the tasks worth cancelling. A cancel arrives as a message, and a message is delivered in a task of its own — so a runner that never returns to the worker’s event loop never receives one, and
throwIfCancelledasked a thousand times about a cancel still sitting in the queue.awaitdoes not help: an already-resolved promise schedules a microtask, and the microtask queue drains inside the task that scheduled it. The reads yield between windows and have always been stoppable; the compute did not — a cubic correlation, a diamond over every bin of every sequence, a pileup over ten thousand windows — so those ran to completion and delivered a result to a reader who had asked them to stop. Every one of them now breathes at the top of its outer loop, and the correlation’s row hook is awaited so that stopping it is possible at all. The track venue still has no cancel channel, which is stated in its own docstring and unchanged. -
A companion
.mcoolwas handed to an analysis at its root. A collection is opened at the root, refused and re-opened one rung down, and it is that descended address the panel actually shows — but the star recorded the URI the panel was built with, so starring one asked the worker for the collection and gotmcool-rootback: the refusal a reader was never meant to see, arriving from a task rather than from a picture. -
A companion accused the files of disagreeing while the lead was still reading. A panel with no block yet answers “1 × 1 cells”, and one cell times a bin size is a span — so a companion that settled first was told this block spans 16.6 Mb × 16.6 Mb where the first map spans 1.0 kb × 1.0 kb. The same name resolved against different sequences, which is one bin of the lead’s binsize dressed up as a measurement and then blamed on the assemblies. Neither panel is compared until both have a block, and the note is re-asked when the lead reports, so a real disagreement is still said — once there is something to say it about.
-
Insulation counted cells a cut had already taken away. A diamond of half-width
wspans separations1 … 2w − 1, andignoreDiagsdrops everything below its value — so the cells it leaves are bounded by the diamond’s span, not by its width. The count capped the ignored triangle atw(w + 1) / 2and stopped there, which is right up tow + 1and wrong past it.minValidis a fraction of that count, so the error did not stop at bookkeeping: betweenw + 1and2wthe threshold was measured against cells that could never be counted and bins with a perfectly good diamond scoredNaN, and at2wand above every bin did — the lane came back empty, with nothing said, which is indistinguishable from a file with no contacts in it. The cells are counted over the span now, and a window a cut has emptied is named: a note where some scales survive, a refusal naming the arithmetic where none do. -
A compartment square a cut had emptied was drawn rather than refused. The same control, with no upper bound, on a task whose sequences can be shorter than the number typed into it: every cell becomes a gap, every correlation is undefined,
correlationMatrixfills the matrix with the zeros it reads no evidence as, and the leading eigenvector of that is a shape with no data behind it. It now says which square, and how many bins were in it. -
The keyboard opened rows the filters were hiding. A table keeps the row under the cursor so that Enter opens it and the space bar ticks it, and a filter that took that row away cleared only its position. The row itself stayed, so Enter navigated to something no longer on screen and the space bar ticked it into a selection the reader could not see — a keypress aimed at the list in front of them, answered with one that is not.
-
An emptied colour-clip box set the bound to zero. A
type="number"input reports''both for a cleared box and for anything the browser refused to parse, andNumber('')is0: clearing a bound set one. On alog10map, whose values live entirely below zero, clearing the high box clipped the whole picture at 1. Refused now, like any other value that does not parse — and the refusal is visible, which it was not:changefires on Enter without moving the focus, and the repaint that was supposed to snap the box back leaves a focused box alone, so the number the reader typed stayed on screen looking as though it had been taken. -
A pooled read drew the wrong column.
SpanScanOptionshad novalue, so every pooled, rolled-up and selection read walkedpixels/countwhatever the request asked for, while the dense path honoured the choice — one view showing two different quantities, decided by whether the block happened to cross the pooling threshold, with nothing said. It is threaded now, in the one place both paths meet. Latent since pooling landed; urgent once the resolution started moving with the zoom, because a quantity that changes as you magnify is not a picture of anything. -
bin × binin the whole-genome view. The overview offered two levels and both put one cell per unit: a contig cell sums a rectangle oflength × length, and a metagenome’s contigs span several orders of magnitude, so the axis spends its width on the shortest sequences and its colour range on the longest. On the 48-MAG mock community that is 62% of the axis given to contigs under 5 kb holding 4% of the sequence, one badly-assembled MAG taking 28% of it for 4.5% of the assembly, and a third of the diagonal coming out empty — a haze where the readable picture was one click away in the genomes table, as “select every row”. That picture is now the third entry in the same picker. It is the pooled whole-file read the app already made, so the map, the resolution and the counts are identical to the selection’s, down to the4,057 × 4,057 bins · effective resolution: 50.0 kb; what is new is that it is reachable from where the question is asked.levelcarries it in the hash like the other two, and the picker is one control shown at whichever level is mounted, so there is always a way back.It is also the default, which makes the other two changes obligatory rather than nice. Landing on a level means it must not read anything on its own: the gate is now unconditional there — the scan view’s sentence, the cost, and the button — instead of appearing only above ten million stored pixels, since a threshold makes the rule hold on large files and lapse on small ones. And landing there means it has to be read the way the other two are, so the axis is labelled and divided by genome rather than by contig, with a label click opening that genome; 2,082 contig hairlines over 48 blocks were a grey wash over the structure the view exists to show.
contig × contigstays one click away — “does this bin hold together” is a question only it answers — and where a catalog advertises a sidecar edge list the default still gives way togenome × genome, which is a small download rather than a pass over the file.A map of a chosen set of sequences is untouched: its axis names what was ticked. So is a plain cooler, which has no genomes and keeps its own view kind, its own level control and its
nnzthreshold. -
A sideways scroll slides along the map. Two fingers across a trackpad mean “along the genome” in every browser there is, and already did over this app’s own 1D tracks; over a map they did nothing, because the handler stood aside so the platform’s back-and-forward swipe would work there as it does over the rest of a page. That is the wrong trade over the one element a reader navigates by sliding — and the swipe still works everywhere outside the picture. Over a lane stack it means the same thing, since a stack is drawn on the map’s own axis.
-
vcycles three pictures instead of toggling two: the map and its tracks, the tracks alone, the map alone, and home. Nothing is hidden, moved or restyled on the way round — the map alone is the stacks drawing no lanes for as long as it lasts — so three presses come back to the arrangement you spent ten minutes on. The button offers the next stop rather than naming the current one, because it is a control and not a status light. -
[and]step between sequences in the 1D browser, as they already did on a map. It was the one view where the plainest thing anybody does with a genome had to be typed into the locus box. -
A track’s floor shows the resize cursor. The double arrow every application uses for exactly that gesture, and the one the map’s own box handles already showed — the drawn rule stays, since it is what says which lane the border belongs to, and the cursor is what says it can be dragged.
-
The O/E toggle survives a navigation, as the turn now does. Both are how a reader is reading maps rather than facts about the block, and looking somewhere else is not a request to stop reading that way.
-
A map with a diagonal opens turned. The band along a genomic axis with its tracks under it is what a contact map is for at this scale, and what every published figure of one looks like; the square is the special view — wanted for the far corners and for reading a comparison across the diagonal — so it is what a reader asks for rather than what they are given. The box opens as wide as the panel allows and a quarter as tall. Only the “no” is written to the link, so an ordinary view’s hash stays short, and a block with no diagonal draws square without recording anything — a metagenome you passed through can no longer switch the turn off for every map after it. And the setting now survives a navigation, as the deck and the second file already did: dragging a box on the map to open a region, or clicking through Chromosomes to another sequence, used to come back square, which is exactly the gesture a turned map is for.
-
The map turns 45°. A contact map is symmetric about its diagonal and the diagonal is where all the signal is, so a square map spends most of its area on empty corners and draws the part anybody reads as a thin band across the middle. Turned, that band is the whole picture: a domain is a triangle standing on the axis, a loop is a dot at the height of its own span, and the x axis becomes genomic position — the same axis a track is drawn against, so a profile under the map lines up with the picture rather than with one edge of it. A true rotation, so a base pair up the side is the same length as one along the bottom and the plot can be measured with one ruler; a half-bin lattice, each source cell filling two adjacent columns, so what the renderer gets is an ordinary uniform matrix and pan, zoom, pooling, clip, colourbar and export are the code they always were. Offered only where the block has a diagonal — one sequence against itself, at bin level — and greyed rather than hidden where it does not, so the answer to “can this viewer do that” is yes, from a map. Shared by a whole deck, like the derivations, because a bar claiming one navigation over a band and four squares is not a comparison.
-
A ruler up the side of a turned map, in distance. The axis that is the whole reason to turn the picture was the one axis with no numbers on it: the row ruler was switched off under a turn, since labelling a distance with coordinates would name places that are not there. It is the same affine line and the same 1–2–5 ladder as every other ruler, with a negative gradient — separation grows towards the top of the picture while a row index grows towards the bottom. The tick spacing could not be shared: the default is the width of a base-pair label, and a band a hundred pixels tall fits one tick at it.
-
A turned map is a rectangle of a size you choose, and the zoom fills it. Drag the top edge for its height and either end for its width; neither moves when the map is zoomed. What the zoom changes is what is inside the box — more or less genome across, more or less distance up — which is also the honest reading, since a shorter stretch of genome has shorter distances above it. Fitted like any other matrix, as it was at first, a band’s height was
depth × zoomand zooming in grew it until it filled the panel: at which point the picture is a square map again with the far corners the turn exists to throw away back on screen. The box opens at a third of its own width — enough band for a domain to be a triangle rather than a smear, short enough that the picture still reads as one strip along the genome; both ends widen it about its middle, so the edge under the pointer stays under it; arrow keys move an edge by a tenth of the box, Shift by a half, and a double-click puts the default back. What bounds it is the room the panel has, measured against the panel and not against the grid the box sits in — that grid is sized by its contents, so a ceiling read from there follows the box down, and every drag inwards lowered the limit to wherever the edge had just got to: the width could be given away and never taken back. The map and its tracks are centred as a group in the room a square map would have taken, so flipping does not resize the panel and a track added below re-centres the map above it. -
The guides on a turned map are a pair of rays, not a cross. Hold one end of a contact still and let the separation grow and the contact walks diagonally across the picture, so the lines that say which two places this is leave the pointer at 45°: a pointer on the map gets an X centred on the contact, which near the axis reads as a V and near the top of the band as a ∧, because the band is all that fits of one. A pointer on a track gets a V standing on the axis under it — the reader is naming one place, and the contacts that place is in are the two rays leaving it, both ways along the genome. And a pointer on the map now puts two hairlines in every track, one under each end of the contact: the column it is over is the midpoint of the two, and no track has anything at it. On the diagonal the two ends are one place and there is one line.
-
Marks on a turned map, placed by the loci they name. Every overlay used to bail out where there was no row axis, so a reader’s own marks vanished when they pressed the toggle and came back when they pressed it again. A line stays a line at the same place on the genome, joining up with the same line in the lane below it — including one drawn as horizontal, whose axis is gone but whose locus is not. A domain becomes a triangle standing on the axis, a loop one dot at the height of its own span, an arrowhead a glyph standing over that dot with its tip pointing back at it. A call further out than the band shows is not drawn at all: that is outside the data, not off the edge of the picture.
-
Every track in one stack below a turned map, whichever edge it was put on, and once even if it was on two. A presentation of the placement rather than a rewrite of it, so everything is back where it came from the moment the turn is switched off — a track that returned on the bottom edge because it happened to be turned when the button was pressed would be the picture editing the reader’s settings behind them.
-
A track goes on any edge of any map in a deck, from one list. A track is the import and the style true of it everywhere; a lane is one placement of it, carrying four edge booleans and almost nothing else. A lane given no patch of its own cannot drift from its track, which is what makes put it on every map different from copy it — recolour the row and every copy moves. One press sends a track to every map at the edges it is already on, and one takes it off all of them while leaving it in the list, with its settings, ready to put back.
-
The session’s heading leaves the deck’s first cell. The title, the locus box, the back link and the block summary answer what am I looking at, which is one question however many pictures are on screen, so a deck hoists them above its grid — and cell 0 becomes a cell like the others, holding a map and its file name. Inside, they made the first cell a head taller than the rest and the first map was drawn smaller than the maps it was there to be compared with. The deck’s count rides on the end of that summary now,
113 × 113 bins · … · 3 maps, rather than in a bar of controls. -
A deck’s bar holds only what is shared: the master chain, one scale for all, how many maps across, and one figure of the whole deck at 300 dpi. Adding a map is the
+on an edge and rearranging is the grip, so neither is a button there — a bar that repeats what the pictures already offer is a second place to look for the same thing. -
Maps are rearranged by dragging them. Every companion has a grip before its name; taking hold of one shows the deck’s tiling, and dropping a map on a cell exchanges the two.
◀▶could only ever say one place along a list, which describes a 2 × 2 poorly: moving the bottom-left map to the top-right was three presses, each shuffling everything in between. The lead has a name like every other cell and no grip — the first map is always the lead. -
A companion’s scale controls are on its picture, in a menu that opens upward from a button in the map’s bottom-left corner: the score, the baseline, the transform, the ramp and the clip, and nothing else. What was there was a
colour and exportfold under the map, which pushed every other panel of the deck down by the height of whatever was open in it. -
- A deck is drawn before it is filled (§16.9). A
+down the right-hand edge of the map and another along the bottom. Pressing one opens a cell rather than a map: what comes back is the arrangement asked for, with the reader’s own map in the first of it and every other cell standing empty as its own drop target. Drop a cooler on one, click it to choose a file, or type a URL in the bar — in any order, or never.
This is the feature’s first entry point a reader can find. The first companion used to be reachable only through the command palette, so nothing on screen said the viewer could put several maps side by side at all — and the one gesture anybody would try, dropping a second cooler on the map, means load a track here.
Cells before maps is the design and not an ordering. Adding maps one at a time makes the arrangement a consequence of the order the files arrived in, and the arrangement is exactly what somebody assembling a figure is composing: two conditions across and two timepoints down is a statement about an experiment, and reaching it by dropping four files into a row and then pressing
◀twice is repairing a layout rather than drawing one.The shape is a rectangle filled in reading order, and a press adds a whole edge — a column beside, a row below. One extra cell on the right of a two-row grid cannot be expressed without pushing every map after it one place along, and a press must never move a map that has already been placed. The column count a
+sets overrides the three layout presets, because the two answer different questions and a deck that reflowed into a row the moment a map was closed would not be an arrangement at all.An empty cell is a real cell: it takes a slot in
maps, travels through a link as the same~a local companion uses, counts towards the six-panel ceiling, and moves and closes like any other. It does not count towards the memory budget — an empty card has no worker and no block, and dividing the allowance by it would read the two maps of a 2 × 2 deck at a quarter each while half went to rectangles. A file dropped on an edge opens the cell that edge names and fills it in one movement.Adding the column is a splice and not an append, one cell into the end of each row. The grid is one flat list in
repeat(n, …), so a cell’s position is derived from its index and the column count and raising the count re-maps every index at once — appending turns a 2 × 2 into a three-wide row with the fourth map stranded underneath, which is precisely the rearrangement the whole gesture exists to avoid. - A deck is drawn before it is filled (§16.9). A
-
N maps side by side, navigating together (§16). Several contact maps on screen at once, each with its own file, its own reader worker and its own colour scale, all showing the same place. Addable, removable, reorderable, laid out as a row, a column or a near-square grid, and carried in one hash key so a deck of four remote files is a link.
The viewer already had two answers to “two files” and this is a third because neither generalises. A tab is a whole second session and only one is on screen, so it answers open rather than compare. The lower-left triangle is two files in one square, which is the strongest comparison available and is why it is gated on a matching bin size and a matching chromosome list. Both are untouched, and a session may hold a blended square and a deck: they are different claims about the files, and both can be true of one screen.
The view is shared and the presentation is not. Which region, which level, which score, which balancing — that is what is being looked at, and four files at four regions are four unrelated pictures that happen to be adjacent. The clip is per panel and has to be:
vmaxis a number of contacts, and two libraries at different depths do not have the same one, so one clip across a deck draws the shallower file as an empty rectangle. One scale is offered for the case where the reader knows the files are comparable, off by default because it is the stronger claim — the same argument the two-file split settles with match depth, reached from the other side.The lock’s currency is base pairs, not matrix cells, and that is the whole of why panels can be locked where triangles cannot. A 1 kb file and a 5 kb file both answer
chrI:20,000-80,000and the answers are 60 cells and 12; a lock in cells moves the coarse panel five times too far and would have to be gated on a matching bin size, which is exactly the refusal side-by-side panels do not need. Conversion rounds outward, so a coarse panel shows at least what was asked for and never less; and a window running off the end of a shorter block is slid back inside it keeping its span, rather than truncated to the overlap, because answering a 40 kb view with a one-bin view is a different zoom in a deck whose whole promise is that the panels show the same thing.There is one owner and panels never speak to each other: with N panels each echoing to N−1 others there are N² conversations and no frame in which the deck is in one state. The feedback loop is guarded three ways, because an echo has three shapes — the synchronous one, the one frame later from a panel that had to slide (which otherwise makes “pan past the end of the short one” mean “you cannot pan past the end of the short one”), and the one that arrives later at the same window seen through a coarser grid. Without the last, a two-resolution deck drifts a cell per frame until it hits a wall.
A companion is asked for its block by name, so a file that has no
chrIis refused byparse_metacooler_range’s own error listing its own sequences. The one gate that does exist compares the blocks rather than the files: two panels that resolved one name to spans of different length resolved it against different sequences, and one window would be two places. A panel the gate refuses is still drawn, still readable and still exportable — only the lock is off, and it says so in a line under its own heading rather than only in a tooltip on a 14 px chain, because a still map reads as data.A local map keeps its slot in a link.
uri2omits a local second file, which is right when there is one slot; with several, omission moves everything after it one place left and a deck of three arrives as a deck of two with nothing saying a map is missing. It is written as~, and the recipient gets a drop rectangle where the sender’s file was. The separator is|rather than the comma the other lists use: a comma is legal in a URI, somaps=a,bwould resolve — to two maps that are neither of the ones that were sent — the first time somebody publishedsample,rep1.cool.N panels cannot silently blow the memory budget.
checkBrowserDenseSizewas written for one block and has no idea how many are about to be live, so the deck divides one panel’s allowance between all of them: a deck can never cost more than the single map it replaced, and every map added shrinks every panel’s share, which is what refuses an over-large deck by arithmetic rather than by a guessed limit. Six panels is the ceiling above that, for the reason twelve sessions is one.The three side panels are untouched. The deck’s controls ride above its own grid and each panel’s colour and export cards ride inside that panel, which is both the plainest statement of what is shared and what is not, and the only placement that works on a file with a genome layer — where the Annotations column does not exist at all (§8.5), and which is exactly the kind of file a deck is most useful for.
-
Values the file does not contain. A cooler stores counts and, if somebody ran
cooler balance, one weight per bin. Everything a reader wants beyond that is arithmetic over numbers already on screen, and none of it needs a companion file, a preprocessing step or a server — so the constraints that shape this viewer permit all of it.Normalisation joins the score picker:
VC,VC √andICE, computed from the block. They are in that list rather than beside it because a reader choosing betweenbalancedandICEis choosing between two answers to one question — whose balancing, the file’s or ours — and becausebalancedis only offered on a file somebody balanced, so a reader of an ordinary file previously had one entry, no control drawn at all, and no answer to “can I see this normalised”.Observed over expected gets a toggle of its own, because it answers “compared to what” rather than “what number is in the cell”, and it composes with every entry in that list —
O/Eof balanced counts is the standard reading, and folding it into the same dropdown would have made the two mutually exclusive for no reason but the shape of the control. Contact frequency falls by two to three orders of magnitude from the diagonal outwards; that decay is the strongest signal in any Hi-C map and the one nobody is looking for, and dividing it out is what makes the departures from it the picture.The comparison becomes a three-way:
split,ratio,difference. The split answers “what do these two look like”; past a certain similarity the eye cannot get “where do they differ” out of two triangles at all, and the other two draw that directly, with every cell where the files agree at the neutral colour. Depth matching is forced on for both and its checkbox hidden — a ratio of two libraries at different depth is that scalar everywhere, a flat wash with the structure invisible, so there the depth is a nuisance parameter with no reading rather than a choice worth offering.Juicer’s
KRis deliberately not a fourth normalisation. It and iterative correction solve the same problem and converge to the same weights; KR is a faster solver for it, and at this scale the fixed point converges in milliseconds, so it would buy a name in a menu and nothing anyone could see. A file balanced withKRelsewhere and read here underICEshows the same map. -
A colour scale that can express a middle. A ratio is not a quantity with a bright end but one with a neutral point, and three things had to change together for that to be drawable — doing fewer would have produced a plausible picture of the wrong thing.
log2joins the transform list, so twice and half sit the same distance either side of neutral; the auto range becomes symmetric about it, so the colour meaning “these agree” is the middle of the ramp rather than an arbitrary point in it; andRdBu_ris the first diverging colormap, red for more. The panel takes the transform, the colormap and the clip when a derivation is switched on and gives all three back when it is switched off, because avmaxof 8 is a sensible ceiling onlog1pcounts and nonsense onlog2of a ratio.Pooling changed with them. A contact map is pooled by its brightest cell, because its signal is the bright cell; a ratio pooled that way shows only the arm above neutral, so every region where the second file is the stronger one would have vanished at fit zoom — half of what a difference map is for.
-
One lane made out of two. The same three operations for the tracks: a ratio, a log ratio or a difference of two bigWigs, from the batch bar when the selection is a pair. Two profiles drawn as two lanes are read by eye, one against the other, and the eye is bad at it — a small consistent difference is invisible under the large shared shape.
Combined on the common refinement of the two interval lists rather than on a grid. Two bigWigs’ intervals do not line up, and evaluating both onto a uniform grid is one line and wrong exactly where it matters: a boundary falling inside a cell is smeared across it, so a sharp edge in a control track becomes a ramp and the ratio against it develops a spike at the position being asked about. Cutting at every boundary of either input is exact and costs at most as many intervals as the two inputs together. A derived lane fetches nothing of its own — both operands go through the ordinary sample cache — so it inherits the pan-and-zoom-free rule from the two it is made of, and removing an operand takes it with them.
-
Each companion picks its own resolution, from the
::1000beside its name. A deck lands an.mcoolon the lead’s bin size (§3.4), which is the right guess and only a guess: two files rarely have the same ladder, and the rung that makes a comparison legible is not always the one that matches a number. The entry is a picker now rather than a fact — the same control the breadcrumb gives the session’s own file, in the only place a companion has for one, which is its heading. The choice is written into that panel’s own address, so it survives every navigation, every change of chromosome and the link: the map reopens at the rung the reader chose, not at the one the deck guessed. Hidden on a plain cooler and on a one-rung collection, where the control could not be used. -
A deck closes from its own bar, with the × at the right-hand end of it — and it takes the tracks that were only there for the companions with it. A bigWig dropped on a companion’s edge is an import into the session’s one track list, so closing the maps it was drawn on used to leave it loaded, invisible, and holding a row that said it was drawn nowhere. A track that is also on the first map is a different thing — it is being read against a picture that is staying — so it keeps its lane there and loses only the companions’. Nothing touches the first map, which is the session’s own view: closing the deck leaves exactly what closing the last companion one at a time would.
-
⌘C copies the whole deck, laid out as it is on screen, at the same 300 dpi the deck’s save uses. The key meant “copy the figure” and copied the lead — one cell out of six, on a screen showing six, which is the answer to a question nobody asked. There is only ever one picture in front of the reader, so there is one command: with a deck up it is the deck, and without one it is the map it always was.
-
Analysis, in the browser. A fourth panel, outermost on the right (
j), lists what can be computed from what is open, and clicking a row opens a drawer to run it in. Fourteen tasks: contact probability P(s) with its log–log slope, coverage and marginals, diamond insulation with boundary calls, a virtual 4C profile, compartments with their correlation matrix, eigenvectors and saddle, metaplots over feature sets, aggregate peak analysis, chromosome interaction domains, a track correlation matrix, interval-set overlap and Jaccard, and four that only a metagenome has — per-MAG contact QC, contig purity and contamination calls, split-MAG detection, and the inter-MAG contact network.Everything about them is a parameter, because the useful version of any of these is the one run three ways: insulation over a doubling ladder of window widths rather than one, an APA normalised against distance-matched shifted controls or against observed/expected or not at all, a metaplot in fixed windows or over scaled regions, a 4C anchor with cis-only or trans included. Every parameter is one entry in a task’s descriptor, and one renderer draws them all — a new task is a descriptor and a function, not a form.
What it is not is a modal. The drawer is a peer of the map: it takes width from it rather than covering it, drags to any width you like, and the map keeps drawing at that width. A plot you are reading and the region it came from are worth having on screen together.
-
A job never blocks the view. The analysis runs in a fourth worker with a mount of its own, so a whole-file scan over tens of millions of pixels — P(s) on a 26 MB MetaTOR output, say — leaves pan, zoom and navigation exactly as responsive as they were. The reader worker is not asked for anything while a job is running, and a job started from one view survives moving to another: the framing is an input, captured when you press the button, and is reported back in the result’s provenance rather than tracking wherever you wandered to since. Progress and cancel are on every scan, the same way §7.3’s overview has always had them.
-
The panel shows the whole catalogue, and says what each task is waiting for. A task used to vanish when its input was not loaded — APA with no loop set, the metaplot with no bigWig — on the argument that a disabled row is a promise the file cannot keep. That argument is right about a metagenome task on a plain cooler, where nothing the reader does next produces a
/genomestable. It is wrong about a task that is one drag-and-drop away: hiding APA does not spare anybody a broken promise, it teaches them the application does not do aggregate peak analysis.So there are three answers now instead of two. Absent where nothing the reader can do would help. Waiting — listed, dimmed, with a line saying what to load — where something would. Ready. A greyed row carrying an instruction is not an unkept promise; it is the menu.
-
The Analysis panel exists without a contact map. Metaplot, track correlation and interval overlap read only the loaded tracks, and a 1D browser session is exactly where a reader has the tracks and not the matrix — so the column was being taken away from the people who could use three of its tasks. There the map tasks are greyed on open a contact map, the track tasks run, the analysis worker is never mounted because nothing asks it for a pixel, and a result’s provenance names the assembly rather than a block.
-
A 4C anchor is picked on the map. Reading coordinates off a picture in order to type them back in beside it is asking twice, so the Anchor box carries a Pick on map button: press it, Shift-drag along either axis, and the box fills. An arm rather than a mode left on — one drag answers it and the button goes out, so a reader who wanted one anchor does not find their next three attempts to zoom quietly redefining it. Escape is the way out, and it works from wherever the focus happens to be, since the button that armed it is across the window from the plot.
What is written into the parameter is the resolved locus, never a “from the map” sentinel — so the record of a run names the place it was a reading of, which is the whole of what the provenance is for. The box stays editable throughout: a picked locus is a starting point for a typed correction rather than a replacement for one.
-
Results come back as tracks. A task that produces one value per bin — insulation, 4C, coverage, an eigenvector — lands in the Annotations panel as a synthetic track, and from there it is indistinguishable from a bigWig you loaded: it has a colour chip, an editable range, autoscale, a height, a place in the stack, a group, and it can be dragged to any of the four edges. It is the same store and the same renderer, so nothing about lanes had to learn what a computed track is.
A task that produces intervals — insulation’s boundaries, CID’s domains — lands there too, as an overlay on the map it was called from, with the same colour, shape and opacity controls any dropped BED gets. Running the task again replaces that row rather than adding another, because the second run is the one you wanted. And it exports as BED (or BEDPE for a paired set), through the same writer the hand-drawn marks use — one opinion about half-open ends in this repository, not two — so a called boundary set goes into bedtools, into IGV, and back into this app as an overlay. That last is the property the test asserts; the text is only how it gets there.
-
A bigWig writer, in this repository. Header, zoom levels, total summary, chromosome B+ tree, data sections and R-tree index, written byte by byte, with
CompressionStream('deflate')doing the zlib framing the format wants — no dependency, nobedGraphToBigWigstep, no server. Any per-bin result exports as a bigWig that IGV, deepTools and pyBigWig open, and the ones that are not per-bin export as bedGraph, BED, or TSV. Plots export as PNG and as true vector PDF, through the sameVectorPagethe 1D browser has used since 0.6.Its tests read every file back with
@gmod/bbi— the same library the app reads other people’s bigWigs with — and then walk the B+ tree byte by byte at depth 3, because no reader in existence compares an internal node’s keys against its subtree and a wrong key is invisible until somebody else’s tool opens the file. -
Compartments can be phased by GC content, not only by a gene model. Which end of an eigenvector is “A” is arbitrary until something outside the matrix decides, and a FASTA is the signal available when no annotation is — so choosing one now fixes the sign, and the drawer says which signal did it and how convincingly.
How it gets there is the interesting part. The residues live in the annotations worker and are megabytes; the task runs in the analysis worker. Rather than give one parameter a third venue, the main thread asks the annotations worker for a sample at the file’s own bin size — the same request its GC lane is already drawn from — and ships one number per bin with the job.
run/compartments.tsnever learns a FASTA was involved. The same path resolves a chosen bigWig, which had been declared since the first milestone and never implemented. -
coolwarm, matplotlib’s diverging blue–white–red, for the signed things: an eigenvector, a log ratio, an observed/expected matrix. Deliberately not added to the colour-vision-safe list — a diverging red/blue ramp is exactly what deuteranopia collapses, and the picker should keep saying so. -
Goldens for the analysis, against four other tools. Eight of them, built by
npm run fixtures:analysisfrom the demo micro-C map with cooltools, coolpuppy, pyBigWig and numpy, and committed. Each carries two oracles: this repo’s own definition recomputed in numpy, compared to equality, and the other tool’s number under its definition, compared to whatever relation actually holds.Three of those relations turned out to be exact and are asserted as such. P(s) equals
expected_ciscell for cell. Coverage satisfiescooltools_cis == ours + diagonal— cooltools sums the pixel table by both ends so a diagonal pixel counts twice, and this repo counts it once on purpose, which the identity now proves rather than asserts. APA equals coolpuppy’s pileup once the loops whose window would cross the main diagonal are left out.Insulation is not one of them, and that is recorded rather than smoothed over: cooltools’ diamond is offset by a row, drops two diagonals, divides by a count instead of summing, normalises by a median instead of a mean, and masks near dropped bins. Both definitions are published; what is left between them is a correlation of 0.61–0.85 on budding yeast at 1 kb, which has little domain structure. The golden records the measured figure per chromosome and window and the test asserts it has not degraded, which is a true statement where an equality would have been a false one.
-
TASKS_PLAN.md, and SPECS §18. The plan is the build document; §18 is the specification the source cites, and it now carries the ten analysis rows of §12’s budget table, the first of which is the one that mattered most: any analysis task, effect on the view — 0 dropped frames, 0 blocked reader requests. -
Analysis outputs name your tracks, not
a1, a2, a3. A task is handed the ids of the entries it reads — that is the right thing internally, since an id is stable across a rename and a label is not — and then wrote those ids into what it produced. So a track-correlation heatmap came back with axes readinga1, a2, a3, a table whose rows were the same, and a metaplot whose series were too: a figure nobody can read, about tracks the reader had named. The runner now sends the labels alongside the ids, and every output uses them. -
An analysis on a
.metacoolkeeps its numbers. A per-bin result is a lane rather than a picture (§18.5), and on a cooler that is exactly right: it becomes a row on the Synthetic tracks card and inherits colour, limits, edges, the tooltip and the 1D browser by being the same object a bigWig is. A metacool carries no annotations at all (§8.5), so it has no such card — and three of the tasks offered on one produce per-bin vectors: coverage and marginals, described in §18.8 as the cheapest useful thing in the catalogue and the one most worth having under the map; diamond insulation, which isREADYthere; and MAG purity’s per-contig track, advertised as “exported as a BED and as a per-contig track”. All three were priced, run, reported as finished — and then dropped, under a sentence telling the reader they were on a card that is not on screen.The drawer now draws the profile itself on that layout, bucketed to the width it has with a min–max band so a spike inside a bucket shows as a band reaching up to it rather than being sampled away — which matters, because a marginal is read to find exactly those. Beside it are the exports the row would have carried: bigWig and bedGraph, written by the same writers the card’s export row calls, plus the TSV, PNG and vector PDF every figure there has. §18.7’s headline — bigWig for a signal, bedGraph, BED, TSV, PNG and vector PDF — is now true on both layouts rather than on one. A called interval set still has no picture to be drawn as, so it keeps its sentence; what it no longer does is claim to be on a panel that does not exist, and it says plainly that the BED beside it is the way to take the coordinates.
-
A gene model shows the gene’s name, and its accession beside it. Half the GFF3 world does not spell anything the way GTF does: a VEuPathDB or PlasmoDB annotation writes
IDandNamewhere Ensembl writesgene_idandgene_name, and types its genes with the Sequence Ontology’s specific terms —protein_coding_gene,ncRNA_gene,pseudogene— rather than the baregene. Every one of those was a place the parser looked for something and found nothing, so a P. falciparum annotation was drawn as 6,193 systematic ids with its 2,578 symbols sitting unread two lines up in the file. The gene line is now recognised whatever it is called, registered under both the identifiers a child can name it by — GFF3’sIDand GTF’sgene_id, which one file mixes freely — and the symbol reaches the transcripts drawn under it.The same pass fixed a quieter one.
transcriptandmRNAwere the only lines read as transcripts, so anncRNA, atRNAor apseudogenic_transcriptwas read as somebody’s child and filed under itsParent, which is the gene, while its own exons were filed under theirs, which is it: two glyphs for one transcript, the second a nameless box with no exons in it. That file drew 402 of them.The name and the accession are now two facts, and both are kept. The map and the browser draw the symbol, because a label inside an eight-pixel box has room for one thing. The card a click opens leads with both —
gene name: LipL1,gene ID: PF3D7_1314600— above the coordinates, because a reader clicking one of the sixty genes calledVARis very often clicking it for the accession. The hover readout carries both. And the search box answers to either:LipL1out of a paper andPF3D7_1314600out of a supplementary table find the same gene, which on an annotation whose symbols repeat sixty times is the only way to ask for one of them. A gene the file identifies but never names is still drawn as its accession, and is printed once rather than twice under two labels. -
A track turned off stays off when the resolution changes. Whether a row is drawn is a setting like its colour and its height, and it was the one that did not come back: the door a row returns through is the same one a new row arrives by, and a new row is drawn. On an
.mcoolthat is not a rare event — the same file at another resolution is a fresh open with the tracks restored around it — so changing the resolution quietly undid a reader’s “not this one”, by way of a menu about something else entirely. -
The 1D browser’s ruler gains digits as it is zoomed. Two decimals of a megabase is ten kilobases, so a window narrower than that on a chromosome measured in megabases wrote the same number under every tick — an axis reading
4.60 Mbfour times over, under a picture zoomed in to answer exactly the question it was there for. The ruler now passes the gap between its own ticks to the formatter, which picks a unit two decimals can tell them apart in and steps down rather than growing a third:4.6 Mb,4605 kb,4601520 bp. The map’s own rulers go through the same call and gain the same digits. -
A derivation over a turned map is drawn at the depth the band has. Turning a map and then switching on O/E painted the near-diagonal half of the band a solid navy block. The band is re-cut behind the picture as the zoom moves, so it is not always as deep as the box would cut it now — and a derivation is swapped in as new numbers in the same grid, so one computed at the box’s depth arrived at a grid of another and was read with the wrong stride: every row a different separation from the one it was drawn at, and a tail of memory nothing wrote beneath them, which colours as a solid block of the scale’s floor. The swap now asks for the grid that is there, and the renderer treats a length that does not fit as the reshape it is rather than drawing it.
-
Zooming a turned map is a zoom again, not a stall. On a whole chromosome at a fine resolution every wheel notch blocked the main thread for about a second: twelve notches drew 48 frames, twelve of them over 50 ms and the worst over 1.3 s. Three things, in order of what they cost. The colour scale was re-measured on every re-cut — a pooling and a percentile selection over the whole square block, four fifths of the total, spent proving the colours were the colours they already were; the block a band is cut from does not change when the cut does, so the measurement now survives the reshape. The band is re-cut only when it is too shallow, since a band deeper than the box is the same picture — it stands on the box’s floor either way and the extra rows are clipped off the top — so zooming in costs nothing at all; and when it is too shallow it is cut twice as deep as asked, so a zoom out is one rotation rather than one per notch. And the rotation itself no longer blanks the whole band before filling it: only the two corner triangles are empty, so the pass that emptied them is over those instead of over the largest array a turned map allocates. The same twelve notches now draw 229 frames, none of them over 50 ms, and zooming back out is the same.
-
A turned band keeps its size across a navigation. The box was a field on the panel, and the panel is rebuilt on every navigation — so a change of resolution, a click on another chromosome or a zoom into a region put the band back at its default, which is to say the reader lost it the first time they went to look at something in it. It is a store now, like the 4C anchor and for the same reason, shared by every map of a deck and remembered between sessions. What is stored is what the hand dragged; each panel fits it to the room it has on the way out, so a box shaped beside a wide map is still the box beside a narrow one and a window made small and wide again comes back to the size the reader chose.
-
There is a pointer again where the guide lines are. Hiding the hardware pointer entirely left nothing to follow between frames — the lines are at the position the app has resolved, and the hand is somewhere they have not got to yet. It is a small open cross now: four short arms around a hole, so the pixel it is about stays visible through the middle, and drawn a little heavier than the hairlines it stands among, since a pointer that reads as one of the guides is a pointer that has to be hunted for. The arrowhead is still the resting cursor everywhere the guides are off, and the transient cursors are untouched.
-
A turned map’s ruler is measured in the same place as its readout. The column axis under a turn counts in half bins, and the halving was applied to where the block starts as well as to the step — so the ruler sat half of the block’s own offset into the contig behind the picture drawn on it. On a window opening 120 bins into a chromosome every tick read 60 kb short, and the tooltip disagreed with the axis beneath the very cell it was describing. Only the step is a cell size; where the block starts is a distance in whole bins, as it is for everything else that reads it.
-
The top edge of a turned band follows the hand that drags it. Screen y grows downward and the box does not, so dragging the top edge towards the top of the panel — which is the whole gesture for “taller” — made the band shorter.
-
The maps in a deck line up again when they carry different tracks. The room a stack takes was reserved from the lead’s geometry, on companions assumed to have none of their own — so a strip above one map and none above another drew the same window at two heights. Every panel now leaves room for the deepest stack on each edge across the whole deck and pads out the difference. Asked of every panel at the moment it is used, not accumulated as they report: they announce in whatever order they finish reading, and a running maximum is a maximum over whoever has spoken so far, which on the first pass reserved nothing at all.
-
The location control reads. A label column and four edge buttons cannot share a sidebar row, so
bottomcame out asbottc— the single thing the segmented control was laid out to avoid — and beside a deck both map names came out as the sharedEcoli.mcool…prefix, truncated at exactly the point where they stop being the same file. The field turns on its side. -
A deck survives the navigations of the file it is beside. Every navigation builds a fresh state, which is right for the region and the level and wrong for the two keys that name what is on screen beside the view — so clicking Chromosomes and then a contig came back with the deck gone and its workers released.
mapsanduri2are now carried across a navigation within one file, by path, so changing the resolution keeps the deck and opening a different file does not. -
An
.mcoolcan be a companion. A collection is not a map at its root, so it opened as an error about::syntax the reader never typed, in the rectangle where their map should have been. A companion now descends into an entry the way the compare card and the picker already do, preferring the lead’s bin size, and publishes what it opened so the link names the map rather than the collection. -
A companion knows it is balanced. Under
log10a balanced cell is negative, and the automatic range keeps negatives only when it is told the matrix is balanced — so a companion built without the flag drew a blank rectangle from a range of[0, ~0]over data that was perfectly good, with a key calling the numbers counts. The score’s own resolution had the same shape of bug one level up: read straight from the state it answeredcountfor a map whose picker saidbalanced, because the default is a fact about the file and is nowhere in the hash. -
A new companion opens on the lead’s score and clip. The score is shared as a resolved choice rather than as the hash key that named it: a balanced file opens balanced and that default is nowhere in the URL, so two panels each applying their own put a balanced lead beside a companion drawing raw counts. A panel that has to fall back to counts — no weight column — keeps the ramp and finds its own bounds, since a clip measured in balanced units would draw it black.
-
The maps in a deck line up to the pixel, tracks or no tracks. The room the lead’s lane stacks take is reserved on every panel beside it and every picture is pushed to the foot of its cell, so a map inset by a stack no longer stands beside one that is not.
-
The
+along the bottom of a map is above the fold. It comes out of the plot’s own vertical budget now; on a map sized to the window it needed a scroll to reach, which is a poor place for the one gesture that says this viewer can show two maps. -
The 45° turn reaches every map in a deck. A companion was never handed the setting, so flipping a deck put the lead on its diagonal and left the maps beside it square — under a bar still saying one navigation. The turn joins the derivations as something the deck shares and a companion applies without offering a second copy of the control.
-
The turn’s own control is a switch rather than a latch. It read whether the map was turned from the presentation object captured when the panel was built, and the session replaces that object on every render — so the button asked for a flip that was already on, could never ask for it to come off, and never lit up to say the map was turned.
-
Two turned maps in a deck show the same window. The navigation lock was told the square block’s bin size and column count while being reported the turned band’s window. A rotated cell is half a bin and the band is
2n − 1columns wide, so a whole-band view came back as twice the base pairs it covers and was clamped to half the columns at the other end: every turned panel drew half of what the panel beside it was drawing. -
A cooler dropped on a deck’s empty cell is not also imported. The content area’s own file handler took the same drop, so one gesture opened the companion the reader aimed at and loaded the file over the lead. The deck’s cells and its
+edges now answer for themselves, as the landing page’s drop zone and the map’s lane edges already did — and the exemption is tested withinstanceof Elementrather thanHTMLElement, since an inline<svg>is neither, and the+glyph is the most obvious pixel on an edge to aim at. -
A refusal to lock a turned deck says what is actually wrong with it. Two files at different resolutions turn to different depths, because the depth is a fraction of each block rounded to a whole bin — a real reason not to share a row window, and nothing to do with the sequences. The panel used to borrow the wording for two files that resolved one name against different sequences, which is a claim a reader may act on.
-
A view mounted without the
+edges is no longer pinned by them. The session keeps one frame for its whole life and it holds the last view it framed, so opening a genomes table left a destroyed heatmap’s canvas referenced until the reader navigated back to a map. -
A turned band stands on the tracks it is read against. A band is a fraction of the block deep and twice it wide, so at anything short of a close zoom it fitted the panel’s height several times over — and it was centred there, which is the square map’s rule and the wrong one here: a rectangle floating half way up its panel with a stripe of paper between its diagonal and the profiles the whole turn exists to line it up with. A band now fills the box it is drawn in (see the box, above), so its diagonal is on that box’s own floor at every zoom — the ceiling for the mirrored half of a comparison — and there is no vertical pan to lose it with, a band dragged off its own diagonal having lost the axis it is measured from.
-
The tooltip’s coordinates on a turned map. The two sequence names and the distance were mapped back through the rotation and the pair of positions under them was not, so the card named the right sequence, the right separation, and a pair of coordinates belonging to neither: a turned cell’s row is a separation and its column is half a bin, and read as block indices they land half way along the picture. Clicking a cell went the same way, and now goes through the same inverse.
-
Both drags on a turned map. Up the side of a band is distance from the diagonal, which bounds no window — so a marquee committed a box over rows the block does not have, and a mostly-vertical 4C drag asked for a band of separations and resolved to nothing at all, silently, because the axis it named is
nullunder a turn. Both gestures now use the axis that has coordinates, which is the only honest reading left. A selection is a full-height band over a stretch of the genome and opens the square it cuts out — the on-diagonal window, without the reader having to hold anything down for it — and a 4C drag names that stretch however the hand travelled. The diagonal lock goes with them: on a turned map the diagonal is the bottom edge of the picture, so snapping a corner to it would be snapping to the frame. -
Turning the map no longer changes its colours. The automatic range is a property of the block that was read — that is already the rule for a square map, where neither a pan nor a zoom re-measures it — and a band broke it by being a crop: it keeps the near-diagonal separations and drops the rest, and the ones it drops are where the extremes live. On an O/E map the effect was dramatic, the range being the 99th percentile of the departure from neutral and the far separations being where a small count over a small expectation produces the largest departures there are: turning the map roughly halved its range, and dragging the depth handle moved it again on every pixel. The band is now scaled by the block it was cut from, so the turn is what it claims to be — a change of geometry and nothing else. (Not, as it looks, the square counting both of its triangles: a percentile does not care how many times each value appears, so that costs nothing and there was nothing to halve. And §7.1 pins the square’s rule to
metacooler show’s to the bit, so the two could only ever be made to agree in this direction.) -
The O/E reading in the tooltip on a turned map. It indexed the band with the square’s row stride, so the multiple under the pointer was a real number from a real cell and not from the cell being pointed at.
-
O/E leaves an unobserved cell as paper. Zero over anything is zero, and on a scale centred at 1 that is the far end of the ramp — so an O/E map of a sparse block came out as a wall of “maximally depleted” over every empty pixel, which is the loudest possible statement of the one thing the file does not say. Nothing seen is what paper means on every other map this viewer draws. An infinite ratio goes the same way, being a fact about a baseline too small to divide by rather than a measurement; a negative one stays, because an extra
/pixelscolumn may legitimately hold negative numbers and blanking those would be this rule deciding what a file is allowed to contain.
Documentation of chromosomix v0.20.0 · built from 40ec1ec · source
