chromosomix 0.11.0
What changed in chromosomix 0.11.0, released on 14 August 2026.
Changed
Section titled “Changed”-
ffolds the Files panel, and the track-fill overlay has no shortcut. The single-letter space was down toqandzwhen a fifth panel arrived, and neither is a letter anyone reaches for a list of files. Between a column — one of five, in the row of marks at the top of every window, whose four siblings areb,a,mandj— and one of eleven overlay toggles whose button is on screen beside the picture it changes, the letter belongs to the column. The fill toggle is untouched and still in the command palette; it is one keystroke that is gone. -
Save writes the whole window, and the file is now called a workspace. The disk button in the top bar used to write the tab in front of you, which made it the third button on that row doing something a reader could reasonably have expected of the other two. It now writes every open tab in one file — each with its view, its tracks, its marks, its bookmarks, its ticked sequences, its table filters and its analysis recipes — and records which one was in front. Dropping it back opens all of them.
The pair of buttons now states a real difference rather than two words for one thing:
Share this session as a link — the tab you are looking at. Save this workspace as a file — every tab, with everything in it.
A link stays one tab on purpose. The advice line already warns past about eight thousand characters and a single session with eight tracks is a few thousand, so a link carrying six tabs would open by telling the reader not to use it. What is new is that both roads carry the same format — a link is a workspace of one — so there is one parser, one version gate and no way for the two to drift apart.
A dropped workspace is added, never substituted. Six open tabs are not closed by a drop; they were the reader’s, and there is no undo. The one place that does replace a window is the offer on the first screen after a browser restart, which has asked first. A window showing nothing but the picker gives that tab up, and a file with more tabs than the window can hold says how many it left rather than dropping them in silence.
The file format is a clean break, and old
.metacooler.jsonfiles stop opening. They are a tab where the reader now has a window, and carrying both shapes would mean two readers to keep in step for the sake of files that are a few weeks old and a re-save away from being current. Saving again from this build is the whole of the fix. The published example datasets and the CLI’sreplaymoved with it —replaytakes a workspace and re-runs the tab that was in front, or the one--tab <n>names.GLOSSARY.mdmoved with it too. It had session file as one of the senses of session and gave the saved file toSessionConfig; there is no such thing any more, so workspace file is now its own entry and a config is what one holds. The glossary’s own rule — where it and the code disagree, the code is right and the glossary is a bug — is the reason that is part of this change rather than a follow-up. -
On a turned map the guide becomes a vertical line while a stretch is being chosen. At rest the guide under the pointer is a pair of 45° rays, and that is the honest answer to what is this cell: a turned map’s two directions are not its two axes, so the cells sharing a place with the contact under the pointer are a ray rather than a column. It is the wrong answer the moment the next press names an interval instead — Shift held, Option held, or a form waiting to be pointed at — because then the reader is not choosing a contact, and the mark that says where an interval starts is the edge it starts on. So the rays collapse to one vertical line at the pointer’s own column, and the tracks beside the map drop back to a single hairline under it rather than marking the two anchors of a contact nobody is selecting. It answers the modifier rather than the movement, so pressing Shift over a stationary hand changes the guide immediately.
-
Windows installers are temporarily not built. The
--winleg of the release workflow is commented out, so releases carry the macOS and Linux installers and no.exe. Nothing about the target changed —npx electron-builder --winstill works from a clone — and the leg is left in place, commented, so restoring it is uncommenting three lines.fail-fast: falsemeant the other legs never depended on it, andcollectattaches whatever artifacts exist.docs/desktop.mdand the release body say so, and both point Windows readers at the standalone HTML page, which needs no installer anywhere. -
A folded column leaves a tab rather than nothing. Folding one used to take it out of the layout entirely, which left the four marks up in the header as the only sign it existed — and a mark in the window furniture says neither which column is folded nor what is in it. A folded column now keeps a strip of itself with a tab standing on the bottom of it, carrying the panel’s mark, its name, the key that opens it and a chevron pointing the way it will open. The tab is turned a quarter-turn anticlockwise, so the name reads bottom-to-top up the side of the window like the spine of a book — which is what buys it the room to be named at all: a strip wide enough for “General” written upright would be a column, and what a folded column has to cost is a strip.
They nest, and it costs no arithmetic: the four columns are already ordered outermost-first from each edge, and a folded strip sits exactly where its column would. Two folded panels on the left are two tabs side by side at the edge of the screen; open General and the Annotations tab moves out to stand against it, while General’s own tab always stands at the screen edge. The right side is the mirror — Analysis at the edge, Aesthetics against it. Nothing is measured, so no tab can be left stranded behind an expanded column. A panel with nothing to show still leaves altogether: a tab is an invitation, and one that opens an empty column is worse than the column’s absence.
-
The panel buttons come after the name in the header, and the strapline is gone. Four unlabelled marks used to open the row, so a reader identified them before reaching the thing that says what they are looking at. They are now a group set off from the title by a rule, and the line that explained the app to somebody who had already opened it has given them its room.
-
General is marked
file-cograther than a fold arrow. Its three siblings are already marked by what they hold —layersfor the annotation stack,chart-splinefor the picture’s controls,beakerfor what can be computed — and an arrow said only “this panel folds”, which is true of all four of them. What is behind this one is the file and what can be read off it. -
A Shift-drag now opens the stretch it named on the diagonal. The gesture names a band of one axis and zooms to it; the axis nobody dragged took the same zoom and kept whatever centre it happened to have. So the same drag over the same stretch of chromosome I landed on a different picture depending on where the view had been panned to — that stretch against a megabase somewhere else — and nothing on screen said which. What a reader dragging a band out of a contact map means is this stretch, against itself, so the other axis is now centred on the same stretch and what comes back is the on-diagonal square.
Unchanged where the block has no diagonal to open, which is a
chrI × chrIIpanel: there the other axis keeps its centre, since pulling it to a bin the block does not contain would only jam it against an edge.
-
A ratio drew “present here, absent there” in the colour that means the opposite.
x / 0isInfinity, and it is a real reading — the strongest statement a ratio can make in the numerator’s favour. The raster tested the pooled value withNumber.isFiniteand sent everything that failed to the bottom of the ramp. That is right for-Infinity, which is what0 / xbecomes onlog2, and it is exactly backwards for+Infinity: the cells where the first file has contacts and the second has none were painted the second file’s colour.The two arms are the same size — on an E. coli pair at 5 kb, 7.7% of cells each — so half of every saturated cell in the picture carried the wrong sign, and the half that was wrong was the half worth looking at. It could only be caught at the pointer, where two adjacent cells reading
0.001 · 0and0 · 0.001were the same shade of dark blue. -
A ratio of two balanced maps was silently rescaled, and the rescale ate the finding.
ratioanddifferenceforced the depth match on and hid its tickbox, on the grounds that two libraries of different depth differ by a scalar and a ratio is that scalar everywhere. True of raw counts. Not true of balanced ones: ICE puts every bin’s marginal at 1, so two balanced matrices arrive on one normalisation with nothing left to correct — on the pair above, the two marginals agree to 0.2%.Matching them again was not a no-op, because
matchGaindoes not measure depth. It measures the 99th percentile, and the 99th percentile of a Hi-C map is made entirely of near-diagonal cells — which is where two conditions differ. It returned 1.478 and shifted every cell of the ratio by −0.56 inlog2, a quarter of the ±1 range such a map is drawn on: a near-diagonal band that another tool puts at +0.60 came out at +0.04, white instead of red, and the same comparison made anywhere else disagreed with the viewer. It also moved with the viewport, since the gain is measured on the block that was read — the same cell reported a different ratio at different zooms.Depth matching is now off by default on balanced values and on as before for raw counts, and the tickbox is offered under all three operations instead of being hidden under two of them.
-
The hover readout printed most of a balanced map as
0.fmt.numrounded to three decimal places, and a balanced cell is a fraction — at 5 kb on a bacterial genome the typical value is a few times1e-4. Two cells three-fold apart printed the same string, so a reader checking a ratio against the counts under it was shown a pair that could not have produced it. Values below a thousandth now keep three significant digits, and below1e-7an exponent. -
Two metagenomes selected together hung the tab. A file with a genome layer has no deck for a second one to join (§16.8), and the fallback for that case was written as “open it as this session’s map” but implemented by calling the dropped-file router — which had since learned to send a map back to the companion path that was calling it. The pair recursed until the stack ran out, in the one place where the fallback was meant to be the calm one. Found while giving the header’s new add button the same rule to follow.
-
A shared link carried three things it promised not to. Two comments in the codebase already stated the rule — a session’s
comparefield says “a shared link still cannot” carry a local second file, and the deck’s own module says spelling a companion’s name into a link “would post a private filename to a stranger, whichshareLink.tsalready refuses to do”. Neither was true. A link carried the local compared file, the deck’s local companions, and (once sessions began recording it) the size and modification time of the sender’s own file.Worse than useless to a recipient: those names are bare filenames, and opening one goes through a grant store keyed on the bare name whose guard compares only the name — so somebody who had ever granted their own
rep2.coolwould have opened their file in the sender’s slot, with every label on screen agreeing. The redaction now happens inside the encoder rather than at its call site, so a second caller cannot forget it, and the test asserts against the encoded payload rather than the decoded object: the point is that the name never leaves the machine, not that the far end ignores it.A local track is kept, and the rule that decides all of it is now written down in both places: a private name is stripped only where the structure survives without it. A dropped companion costs a label — the
~placeholder keeps the slot, the count and the order — while a dropped track costs the row.
-
Connect a folder, and browse it in a panel — a directory on this machine, or a bucket, read the same way. Every door to a file this app had asked which file once and then closed: the picker, the header’s
+, the browse button beside a URL box. That is the right shape for opening one thing and the wrong shape for the way people actually work with a directory of data, which is to open one, look, open the next, and keep going for an afternoon. Files on a bucket could only be added one at a time, and a folder on the disk could not be browsed at all.A Connect button now sits beside the
+in the header — a plug, because what it attaches is a place rather than a file — and what it fills is a fifth side panel, Files, folded withf. Inside it: the folders you have connected, and the tree of whichever one you are in, with crumbs and a filter. Every file row loads into the tab you are in when you click it or press the→at its end, and every file row can be dragged — onto the map’s edge rectangles for a lane in that stack, onto a companion for a track on that map, onto the Tracks card, onto the page. A dragged row carries a folder and a path rather than bytes; the file behind it is fetched at the drop, once.A local folder and a bucket are one thing to everything above the byte layer.
browse/folder.tsis the whole interface — given a path, the sub-folders and the files; given a file, how to open it — and the two backends are the entire difference between them. It is one column, one tree, one set of crumbs, and one filter, over both.What a local folder means depends on the browser, and the row says which you got. In Chrome, Edge and the desktop app it is a real connection: this browser keeps permission to list the folder and read what is in it — not the files themselves, and nothing is uploaded or copied — so it is still there after a reload, and it picks up a file a pipeline wrote into it five minutes ago. Firefox and Safari have no such thing; there the folder is a snapshot taken when you connect it, marked this visit on its row, gone after a reload. Offering it in two browsers and not the other two would have taken a working gesture away to avoid writing one sentence.
A connected folder is a wider grant than a chosen file, so it is listed in About → what is stored beside the files you have allowed, disconnectable one at a time and swept by forget everything. Nothing about §11 changes: a local folder is read in place by code on your own machine, a bucket is read by your browser from the bucket, and there is no server in between because there is no server.
-
A public bucket needs nothing but its link. A bucket entry used to demand six fields, five of which are a key — so a reader with a URL out of a paper met a form that read as you need an AWS account for this. Public is now the default and it is one field; a key is behind a switch. Everything under a keyless prefix is listed and read unsigned, which is what public data always took.
One thing to know before pointing it at a bucket: listing needs
s3:ListBucketgranted to everyone in the bucket policy, which is a different grant from thes3:GetObjectthat makes files downloadable, and most published buckets grant the second and not the first. A public bucket that refuses to be listed is an ordinary configuration rather than a fault, there is nothing an app with no backend can do about it, and the message says which of the two grants is missing and that files under the prefix still open by URL.docs/hosting.mdhas the policy to paste. Browsing a public prefix needs no CORS change: an unsigned listing is a simple request, so it is never preflighted, and the policy that serves public downloads already answers it.The dialog behind private buckets… is now just Buckets, since it lists both kinds, and its add form is the one the Connect dialog uses — one implementation, so the two cannot come to disagree about what a valid entry is.
-
correlation, a new entry in the score picker: the square replaced by the Pearson correlation between its rows. Two bins correlate when they contact the rest of the sequence in the same pattern, so the checkerboard this draws is the A/B compartments — and it is drawn through the samecorrelationMatrixand the sameusableBinsthe Compartments task solves for its eigenvector, not a second implementation “for display” that would agree with the first until it did not.It is one entry rather than three controls, and it belongs in that list for the reason O/E is kept out of it. O/E is excluded because it composes — the standard pipeline is O/E of balanced counts, and folding it in would make
balancedandO/Emutually exclusive for no reason but the control. The correlation composes with nothing: the cell no longer holds a contact at all, it holds a coefficient between −1 and 1 for a pair of bins’ whole rows, and the pipeline that produces the compartments is fixed at balanced → O/E → correlate. So the entry carries all three steps and the reader picks one thing; a control per step would be three controls with exactly one legal combination. Offered only on a file that carries weights, since a correlation of unbalanced O/E is a correlation of the coverage bias — which is per-bin, and therefore looks exactly like a compartment. The caption names every step that was applied.A dropped bin comes back as a gap, not as a zero. A zeroed row asserts that its bin is uncorrelated with everything, which is a strong and false claim; on a map it would be a stripe of definite colour across the checkerboard. The scope line says how many were dropped, because a blank row and a sparse corner look the same.
It is the one cubic step in the viewer, so it runs on the analysis thread — its own client, not the drawer’s, so a correlation never queues behind somebody’s insulation pass — reports a percentage, and is cancelled by picking another score. Measured at 32 ms for 232 bins, 232 ms for 464 and 2.8 s for 929, so the map caps at 1,500 a side.
Where it cannot be drawn — a second file open under
ratio, acontig_A × contig_Bblock whose rows and columns are not the same bins, or a square past the cap — the map falls back to the O/E underneath it and the caption says so. That is deliberate: the O/E is a real picture and looks like one, so nothing else on screen would betray that it is not what was asked for, and the caption is the only part that survives an export. -
A despeckle, in the
smoothpicker beside the transform. Each cell replaced by the mean of the cells around it with its own value left out —terra::focalwith a zero centre weight, which is the filter Hi-C figures are usually made with. Off,3 × 3,5 × 5,7 × 7; carried in the link, and written into the colourbar caption (counts · smoothed 3 × 3), because a smoothed map otherwise looks exactly like a map.Leaving the centre out is what makes it a despeckle rather than a blur: a cell that disagrees with all eight of its neighbours is evidence about the neighbourhood and not about itself, and a filter that kept a ninth of its own value would carry a wild pixel into the result at a ninth strength instead of removing it.
It is applied per file, before two files are combined, which is what makes it useful on a ratio rather than merely cosmetic. Smoothing the ratio would have to average the
Infinityand0thatx / 0and0 / xproduce, and one infinite cell poisons every window it falls in; smoothing the inputs makes those cells finite before the division, because a zero cell with contacts around it is no longer zero. On the E. coli pair at 5 kb it takes the saturated cells of the ratio from 15.3% to none at all at3 × 3, while the 0.43% that are genuinely unbalanced bins stay blank.Two departures from the R original, both about edges. The divisor is the number of cells that contributed rather than the fixed
(2r+1)² - 1, because here the boundary is the viewport and the alternative is a dark frame that moves when the map is panned; and aNaNcell keeps none, because filling it from its neighbours paints a measurement over the one place the map is saying it has none.Cost is flat in the radius — a separable running window rather than
(2r+1)²reads per cell — so7 × 7is no dearer than3 × 3. -
A blue–white–red diverging ramp,
BuWtRd. Higher contrast thanRdBu_rand with more of its range spent away from the centre, which suits a sparse ratio map: most cells sit near neutral, so a ramp that stays pale through the first third of each arm spends its colour where there is least to say. Its white is anchored to the middle of the scale rather than to the fifth of its eight stops — spread evenly that would land at4/7, and a map of two identical files would have come out faintly blue everywhere. -
A button that adds a file to the tab you are in. Dropping a file on the page has always added rather than replaced — a second cooler comes up beside the first, a bigWig joins the tracks, a
chrom.sizesanswers a browser’s guessed lengths — and that is the behaviour the whole content area has taken since decks existed. What it never had was a button. Every click-sized door to a file went the other way: the file crumb, the brand, Open a different file and a new tab all replace what is open. So the commonest thing a reader wants to do with a second file was reachable only by dragging one in from a window, and the controls that looked like they would do it did the one thing that loses work.The header now carries it, past a divider of its own after the four panel buttons: a file with a
+on it, in every view, opening the picker over what is on screen. It is the same picker the file crumb opens — the drop area that also takes a click, the URL box, the way into a private bucket — with the published catalogue left out, because every entry on that list is a whole session and offering it here would be offering to throw the tab away from the one control whose promise is that it will not. The mark is a file with a plus rather than the bare+one row below it in the tab strip, since that one adds a tab and this one adds a file to the tab you are in.A URL typed into it takes the routing a dropped file takes, which it could not before: a map goes into the deck beside the one that is open, anything else goes to the tracks, and a bucket-browsed file carries its own key there. On an empty tab the distinction does not exist — there is nothing to add to — so it opens instead, exactly as the landing page does, rather than taking a lone bigWig and finding no track list to put it in.
-
The picker now says which file it wants back, not just its name. A session saved from a local file names it by its bare filename, and
rep1.coolis a name a reader may well have several of — so the note asking for it now carries the size and the modification date the session was made with. That is the difference between finding the right file and finding a file with the right name.And when a file arrives whose name matches but whose bytes do not, the app says so once and opens anyway. That is either the same file re-run through a pipeline — legitimate, and worth knowing, because the numbers on screen are then not the numbers in the figure — or somebody else’s file of the same name. The two are indistinguishable from inside the app and one of them is a thing people do on purpose, so it is a remark and never a refusal.
Saying it needed a third way to speak. The shell had a modal, which takes the keyboard and demands an answer, and the error view, which replaces what is on screen; both are far too loud for “this opened, and here is something you may want to know”. Notes now appear as a dismissible row under the tab strip, where the restore offer already lives.
-
Closing the browser no longer ends the session — it offers it back. The tab strip has always lived in
sessionStorage, which is a deliberate rule and not an oversight: a set of open files is a train of thought, it belongs to the window it was opened in, and closing the tab is meant to end it. That rule is unchanged, andsessionStoragestill wins whenever it is present — a reload reads it and never looks anywhere else. What is new is an answer for the one case it cannot cover, because the storage is gone with the window: the browser was closed.Alongside it there is now a durable record — every tab, its file, its tracks with their colours and limits, its marks, bookmarks, ticked sequences, table filters and analysis recipes — written to IndexedDB on a debounce and flushed when the page goes away. On the first screen of a browser that was closed with work in it, a row above the picker says what was open and when, and offers to bring it back.
Offered, never applied, and that is two decisions rather than one. Silently reopening six files a week later reverses the decision the reader made by closing them; and on a shared machine it would put somebody’s unpublished figure on screen for whoever sat down next, which is the reader §11 is written for. Start fresh is a real answer and it deletes the record rather than hiding the note, because an offer that returns tomorrow having been declined today is not an offer.
The record holds no file contents and never will. A local file still comes back detached, exactly as it does from a reload, so this stage changes nothing about what is read or copied — §11 and §12 are untouched, and every browser gets it, not only the ones with a file-handle API.
Two traps are worth recording, because both failed silently and both were found by driving the real application rather than by reading the code.
A restored tab is primed — it holds a view state and has read nothing — and a config is built by reading that tab’s session memory, which after a browser restart is empty. So the first click would have gathered eight empty configs and written them over the good record. The workspace is therefore applied into memory before the first tab is primed, which is also the order
applyConfigalready required for its own reason.And a window with nothing open is two different facts. After the reader closes their last tab it means forget this; at startup it means nothing has been restored yet — and acting on the second deletes the record while the read that would have offered it back is still in flight. A race, resolving differently on a warm cache than a cold one, so the workspace disappeared on some loads and not others with nothing in the console either way. The rule is now a pure function with a test rather than three conditions in the shell.
A third thing that had to change for any of it to be true: the record follows the workspace, not the view state. Loading a track, placing a mark and saving a bookmark are most of what a session is and none of them navigate, so a record written at the last navigation came back missing everything the reader had done since.
-
A local file you have already opened reopens itself after a reload. In Chromium and in the desktop build, dropping or choosing a file now also hands the page a
FileSystemFileHandle— a capability, not a path and not a copy — and that is what is kept. A reload, a restored tab or a reopened workspace resolves the grant and reads the same file, with no drag and, while the browser session lasts, no prompt.Nothing about how the file is read changes: what comes back is the same
Fileobject a drop produces, read a slice at a time by the sameLocalFileSourceas before. No bytes are stored, nothing is uploaded and nothing is duplicated on disk, so §11 and §12 stand exactly as written. Firefox and Safari have no such grant to give, and there a local file comes back detached as it always has.The tracks come back too, which is the larger half of what a reload used to cost: a map is one drop and the bigWigs beside it are eight. A restored track with a grant behind it says reopening… and fills itself in, keeping its id — so it does not jump to the bottom of the card — along with its colour, its limits, its height and its edge. They resolve in parallel rather than one after another, and a grant that turns out to be spent puts the row back exactly where it used to land, with the re-open this file button that has always been the way back.
Two things this deliberately does not do. It does not put local file names into the URL hash: this browser being able to reopen
rep1.coolsays nothing about anybody else’s, so the shareability test the hash is gated on is unchanged, and a link still carries only what a recipient can act on. And it does not ask for permission on the boot path, because a permission request without a user gesture resolves without ever showing a dialog — the reader is asked when they click, and never by a page that is merely loading.A grant is keyed by the file rather than by the tab, so two tabs on one file share it and one
.mcoolat five resolutions is one grant. The row also records the file’s name, size and modification time, because a local file’s URI is its bare name and tworep1.coolfrom two directories would otherwise collide: a name that no longer matches is refused outright rather than opened, since showing the wrong file with every label on screen agreeing is worse than asking for it again. A file that has merely changed is not refused — a re-run pipeline overwriting a file somebody is reading is a legitimate thing to do. -
The deck and the comparison come back too, and the tracks land on the right panels. These were the two files a reload lost most quietly. A local companion is destroyed at serialisation —
mapswrites~for one — and a local second file was never in the hash at all, so neither could even be refused: by the time anything could ask, the name was gone.Neither is fixed by changing the hash, and that is the design. The
~is a promise to a recipient (§16.6): it keeps the count and the order, so a shared deck arrives with a drop rectangle where the sender’s file was rather than silently one map short. Putting a filename there instead would post a private name to a stranger and leave their deck trying to fetch it. So the names live beside the hash, in the session’s own memory, as private as the files they name — which means a plain reload restores them as well as a remembered workspace does.A stale note can only ever degrade to today’s behaviour: a remembered cell that now holds a published map is left alone, and one past the end of a deck that has since shrunk is dropped. A companion whose file cannot be re-opened comes back as a cell that names it, rather than as the link’s “a map was here” — which is true of a stranger’s deck and misleading about a file on your own disk.
And where a track was drawn is remembered with it. Until now a restored deck put every track back on the first map, however carefully they had been arranged across the panels. What is stored is keyed by the file each companion was showing rather than by its panel id, because panel ids are minted per deck and one written down today names nothing tomorrow — so it is seeded into the same parking mechanism that already hands a lane back when a map reopens its file. Where the companions do not come back, the tracks park onto the lead exactly as they always have.
-
A screen that says what this browser is remembering, and a button that ends it. Reachable from the command palette. It lists the durable workspace, the per-window entries, the preferences, and — named outright rather than counted — any bucket access keys, which are stored in plain text and which a reader deciding what to clear on a borrowed machine should not have to press the button to discover.
Forget everything means everything: both web storages and both IndexedDB stores, swept by one rule over the prefix every key this app writes shares. A rule rather than a list of names, because a list only covers the keys somebody remembered to add to it — and a sweep that quietly missed one while telling the reader it had cleared everything would be worse than no button at all. A test reads every
setIteminsrc/and holds the rule to it. -
Recentre on the diagonal, a button in Aesthetics → Map between the turn and
O/E. It slides the view onto the nearest on-diagonal window at the zoom it is already at — the perpendicular foot of the picture’s own centre on the line the two axes name the same bin, which is the least it has to move — without touching the region or the resolution.Every gesture that lands off the diagonal takes several more of the same gestures to come back from, and none of them can be asked for directly: a pan is relative, a box is where the hand drew it, and a wheel zoom is around the pointer. There is no drag that says here, but on the diagonal. Greyed where the block has no diagonal — the trans corner of two chromosomes — and while the map is turned, where the picture already is one.
-
Rectangles, in the Manual annotations card. The card drew two of the three things people put on a contact map by hand: a line says this coordinate and an arrowhead says this cell, and there was no way at all to say this block of contacts — which is the mark a domain, a corner peak and a pair of loci worth boxing all want. The workaround was four lines making a box out of crosshairs, which is four marks to move and four to delete.
A rectangle is two intervals, one per axis, rather than an interval and a shape. That is what makes on- and off-diagonal the same mark and any ratio expressible:
I:120000-180000against itself is a domain, two short stretches a megabase apart is a corner peak, and a short stretch against a long one is a locus against a whole arm — a thing no square could have said. Typed with the second box left empty means the same stretch on both axes, the same courtesy the arrowhead form does for a cell on the diagonal.Drawn with a drag, which is the gesture a hand already makes for a box: press at one corner, let go at the other, with the map’s own marquee and its coordinate chips showing what is about to exist. The marquee follows the hand on both axes — a selection is squared because it resolves to one range, and a mark does not — and it snaps out to the edges of the cells the two ends are in, which is what commits. Holding Shift locks the box to the diagonal, so a domain is one gesture. The mode stays on afterwards, as the other two do, because marking up a figure is several marks — and a click while the map is waiting for a drag places nothing rather than navigating away from the picture being drawn on.
On a turned map the box is off the diagonal if that is where it was drawn. Both ends of the drag are taken back to the square the band was rotated out of, exactly as a click is, so pressing over one contact and releasing over another names the rectangle those two contacts span — the one thing a band selection there cannot say, since a selection over a turned map is always a stretch of the genome against itself. Shift there collapses each end onto its own midpoint rather than snapping the pointer, which gives the on-diagonal square through the two places the drag ran between. And what is drawn while the hand moves is the mark itself, through the code that will draw it for good: a rectangle of rows against columns leans at 45° on a turned map and folds where it crosses the diagonal, so a rubber band would be a box drawn around a shape that is not one.
Which gesture a form asks for is now part of what it hands the map: the two marks that name a place take clicks, the one that names an extent takes drags. A plain drag draws the box rather than panning, because a reader who has just pressed a button that says draw on map is going to drag on the map, and asking for a modifier on top of an armed mode with a lit button and a changed cursor is asking twice.
On a turned map a box is drawn as the contacts it names rather than as its own outline, which is the same rule every other mark there follows: off the diagonal that is a parallelogram leaning at 45°, and on it that is exactly the triangle a BED domain gets, because an on-diagonal box is a domain drawn by hand. A box drawn across the diagonal has an image on each side of it; where they genuinely fall apart — a locus against a whole arm turns into two stripes leaning opposite ways — both are drawn, and where one falls inside the other, which is every box drawn near the diagonal, only the outer one is, since a second outline in there is the edge of nothing.
Boxes are anchored in the lane stacks the way loops already were, a triangle per edge on each axis: a domain boxed on the map puts its two boundaries under the tracks, in the coordinate the tracks are drawn in, which is where the comparison is actually made. They go out with the loops as BEDPE, whose six coordinate columns are exactly a pair of intervals — the row is the box rather than a summary of it — and they are carried in a session file and in per-file session memory like every other mark.
-
bigWigs, gene models and interval sets on a metagenome. §8.5 gated every annotation card on the file having no genome layer, so a
.metacoolhad no Tracks card, no drop zones and no way to draw a coverage track beside the map it was computed from. The argument was that a metacool’s axis is contigs from thousands of independently assembled organisms, so a file keyed on contig names either coincides or misleads.The premise was wrong about the files people have. A metagenomic Hi-C dataset is one co-assembly binned into MAGs afterwards: the names in
/contigsare one namespace — on the reference file, 20,566 sequences with 20,566 distinct names — and a bigWig over that assembly is keyed on exactly those names, in the (contig, start, end) coordinates every other lane here is drawn in. The naming ladder (§5.9) was already inert on opaque assembler ids, which is what makes the rename safe on a metagenome in the first place.So the whole column is offered on every container. What a metagenome needed instead of a gate was an honest report: a track over a co-assembly covers the organisms it was made about, and the sequence counts state that as
794 of 20,566, which is arithmetically true and reads exactly like a file that failed to match. Where the map has genomes the sentence now counts them — 794 of 20,566 sequences, in 4 of 48 genomes: metator_00001_00000, … — and says it as a note rather than a warning, because partial coverage is the expected shape of a track here.__unbinned__is not among them (§7.4). The two real faults keep their voice: no overlap at all, and a cross-convention match whose lengths disagree.Measured rather than assumed, on the 48-MAG reference file with a bigWig written over four of its MAGs: the whole-metagenome picture costs the track three range requests, because the sample cache asks once per view and bbi answers two thousand regions out of one zoom level. No cap was needed.
-
A band dragged across several sequences no longer names a region that does not exist. The reading took the sequence’s name from where the hand went down and the end from where it came up — one contig’s name against another contig’s coordinate — which on a whole-metagenome view, where a cell is a contig or two and any real drag crosses boundaries, produced things like
NODE_…:1-260,000for a 200 kb sequence. The range parser refused it: Genomic region out of bounds. So the 4C anchor could not be picked on the picture it is most naturally picked on, and the same gesture put an unusable string on the clipboard. A band is now clamped to the sequence it began in, and never runs past the end of it. -
4C on a metagenome reads the whole community, and asks one question. A MAG’s contacts with everything else in the tube are the reading a metagenomic map exists for, and the task was giving back the other one: a profile restricted to the anchor’s own assembly, with the rest of the file left as gaps — one organism pictured as though it had been sequenced on its own. So on a
.metacoolthe run is now always the whole-file pass, cis and trans together, across every contig.Cis only and Normalisation are gone from the form there, and only there. On a cooler both are real questions and both are unchanged: a chromosome is a self-contained thing, cis is the commonest reading and the bounded one, and that chromosome’s own P(s) is the curve to divide by. On a metacool neither survives contact with the file — an anchor is one MAG among hundreds, and a decay accumulated from one assembly’s contigs is not an expectation for contact between assemblies, so the ratio would carry two meanings in one track.
The runner settles both keys itself on such a file rather than trusting what a stored session or a form filled in on a cooler left behind, and the estimate prices the pass it will actually make instead of the four range requests the absent toggle would have implied. The arithmetic of both switches is untouched and still goldened; what varies by file is which of it is offered. A stored session carrying a key for a control the form no longer has at all is dropped when the form resolves its defaults.
-
A metagenome is read by genome, and now the gestures agree. Four things on a
.metacoolsaid otherwise.A click on the whole-file map. Its three levels are one picture at three depths and are labelled and divided by genome at all of them, but only two of them resolved a click that way:
contig × contigandgenome × genomeopened a MAG’s own map on the diagonal and the pair off it, whilebin × bin— the default, the level a reader lands on — opened a region view of the twoNODE_contigs under the pointer. All three now dispatch on one rule, so the same gesture means the same thing at every depth (§7.3).The 4C task on those pictures. The runner has always accepted a whole genome as an anchor — cis then means the MAG rather than whichever contig the first bin fell in — and three things upstream of it disagreed. The estimate refused the name with this file has no sequence called…; the locus box suggested twenty thousand contigs and no MAG; and at genome level the form invented a default, reading an axis of 48 genome slots as 48 bins and offering
NODE_17248_len_278011:1-48,000— a real window of the file’s first sequence, chosen by nobody, ready to run. All three are fixed, and Pick on map now works there too: a drag on an axis of genomes names the genome, which is the only thing it can honestly name (§8.9).The Contact maps card and the deck. Neither has ever worked on a metacool —
compareRefusalsays so, and §16’s whole premise is a coordinate system two panels share, which two independent assemblies have not — but the card was still the first thing in the Annotations column and the+edges were still on the map. A control whose only function on this file is to invite a gesture and then explain that it does not work is worse than no control: both are absent now, along with the “add a map” command and the deck a sharedmaps=link would have built (§8.5). -
The map’s own metadata, as lanes beside it. Every file this viewer opens carries tables about its own units — a
.metacool’s/genomeswith completeness, redundancy and GC content in it, its/contigswith the MGE calls and bin ids the annotator joined on, every cooler’s/binswith the weight it was balanced with — and until now all of it was readable only as a table, one row at a time, in a view that is not the picture.A new Metadata annotations card, between the tracks and the marks, lists what the open file carries and draws any of it beside the map. The columns are discovered rather than declared, so a file with a column this app has never heard of offers it like any other. A row is an ordinary annotation from the moment it is added: same colour picker, same rename, same height, same limits, same four edges, same multi-map placement, same reorder, same tooltip, same figure, same session memory — because it joins the store a dropped bigWig joins, which is the argument §18.5 already made for computed tracks.
The ink follows the data. A quantitative column is bars against a value axis; a qualitative one is runs of colour with the category written into the runs wide enough to hold it, from a fixed colour-vision-safe palette — the colours belong to the categories, not to the lane, because they have to mean the same thing in the lane above the map and the one down its side. Which of the two a column is comes from what is in it, so a cluster id written as small integers is a set of labels rather than a barplot of the numbering.
What is offered is what the view can show. One cell of a genome × genome overview is a whole MAG and one cell of a bin × bin view is a bin, so a genome column draws on all three levels and a bin weight draws on one. A coarser column is broadcast down, which invents nothing; a finer one is refused upward, because rolling bins into a MAG means choosing a statistic nobody asked for and then drawing it in the same ink as a number the file wrote. The drop-down lists the ones this view cannot show disabled, with the reason, and a lane whose view has since coarsened says so in its row rather than vanishing.
A metacool has an annotations column for the first time. §8.5’s gate is about files — a bigWig keyed on contig names from thousands of independently assembled organisms either coincides or misleads — and it never covered a column of the map’s own tables, which cannot be matched to the wrong sequence because the names are the map’s. So a metagenome now gets this card, and only this card.
/binsstays bounded, as §3.3 requires: what is read when a file opens is the column names and dtypes, and a column is then read over the bins the picture covers, widened by half so panning costs nothing. A window too large to read at once is refused in the row, with the number.Giving a metacool a store had one consequence worth naming: the analysis drawer decides where a computed result goes by asking whether there is somewhere to put it, and “a store exists” is not the same question as “a lane can draw”. A profile’s values live in the annotations worker, which a metacool still has none of, so the question is now the second one — profiles stay in the drawer that drew them, and a called interval set, whose coordinates are on the entry itself, becomes an overlay on the map (§18.5).
Each column exports as a TSV at its own granularity — one row per genome, sequence or bin, under
#lines naming the file, the table and the column — because a lane is drawn at whatever level the view is, and checking a bar against the file means having the rows the file wrote. -
Every box that takes a file URL now has a
browse…beside it. The bucket browser existed and was reachable from one place: the picker’s private buckets… dialog, the screen for typing secret keys into. So a reader with a map open and four bigWigs on a private prefix had to go back to the landing page, open a credentials form, scroll past an access key to the folder view underneath it, and pick a file — which then opened as a map, because that dialog belongs to the picker and the picker opens maps. The way to load them as tracks was to copy four URLs out of the AWS console.The folder view is now a component and the four URL boxes all offer it: the picker’s open a remote file, the Tracks card’s fold, the Contact maps card’s fold, and the deck’s add row. Each opens the same listing and does its own thing with what comes back — a track stays a track, a map added to the deck lands in the next empty cell the reader left. The credentials dialog keeps it inline, where it still doubles as the check that a key just typed works.
A picked file is opened with the key that listed it, rather than by the lookup at the worker boundary. That lookup resolves the same key today, since a listed URL is under its scope’s prefix by construction — but two scopes may overlap, longest-prefix wins between them, and a file reached through one key should not open under the other.
There is a filter over a listing, and it is a filter rather than a search:
ListObjectsV2matches a key prefix and never a substring, so what it narrows is the level on screen. The line under the list says how many of how many are showing, because the failure worth designing against is a reader reading three rows out of twelve as a folder holding three files. Truncation outranks it: a prefix past a thousand keys is the one case where the list is not the folder. -
A second map can be opened by URL, from the deck itself. The boxes existed — a file chooser, a URL field and an
add— and nothing on screen opened them: the row was revealed by the command palette alone. So a reader with a published cooler and a deck in front of them had a drop rectangle for a local file and no way at all to name a remote one, which is the road §11 exists for. The deck’s bar now opens with add a map…, and every empty cell carries its own folded or open one by URL under the drop hint — with abrowse…beside it, like every other box in the app that takes a file URL. Typed there it fills that cell rather than the first empty one, which matters in a 2 × 2 with three gaps. The box lives inside the drop zone rather than under it, so an empty cell stays exactly the size of the map it stands in for.It is the half that makes a shared deck repairable. A link carries a local companion as
~(§16.6), so the recipient meets a cell they could previously only fill from their own disk — and the deck they mended could not be sent on. -
Each map of a deck can be read as its own quantity. A companion was built with no score picker, on the argument that the score is one of the things a deck shares. That is right about the default and wrong about the choice: four replicates should be one quantity unless somebody says otherwise, and “unless somebody says otherwise” needs a control to say it with. Two ordinary cases had none — a companion carrying a
/pixelscolumn its lead has not got, and a pair a reader wants at two normalisations — and both ended at a picker that does not exist.So the corner menu on each map now holds a score, built from that file’s own columns, starting on whatever the deck resolved. Picking one takes that panel off the shared answer and leaves every other panel where it was; the clip goes back to auto with it, because a clip is a pair of numbers in one score’s units and a balanced matrix is fractions where counts are integers. A panel nobody has touched follows the deck exactly as before, and its picker follows too — the lead’s own control, a link and the back button all move the shared score, and a dropdown left on the previous answer is a control lying about the map it sits on.
-
Pointing at a track is answered on every map on screen. The deck already echoed a pointer on a map to its neighbours (§16.3) and had nothing at all for a pointer on a track, which is half the gesture: a hand moving along one profile asking “what is here” is asking it of every profile in the deck. Now the stacks that measure the same axis get a tick at that place, and each map gets the small form of the guide the panel under the pointer is drawing across itself.
On a turned map that mark is deliberately not the little cross the cell echo leaves. A place is not a contact: the contacts it is in leave the diagonal along two 45° rays rather than along an axis, so a cross there would be two arms pointing at nothing — the horizontal one running through contacts between two other places, the vertical one through a column that is only a midpoint. The mark is the first few pixels of that V, standing on the diagonal under the position being pointed at. A square map keeps the cross, where its two axes really are the two directions the place is an anchor along.
-
chr1,1,01,IandNC_000001.11are one chromosome. Two files of the same genome disagree about its names more often than they agree — UCSC writeschr1, Ensembl writes1, a yeast cooler writeschrIand thechrom.sizesbeside it writes01— and until now every one of those was a different sequence to this viewer. A.hicoff 4DN put beside a.coolout of a local pipeline gave a blank rectangle under a bar claiming the deck was one navigation, and the only fix was to go and rewrite one of the files.So a deck of files that spell their chromosomes differently is now one deck, a track is renamed into the map’s vocabulary as it loads, the locus box takes whichever spelling you have in your hand, and two coolers of one assembly are no longer refused the blended square for the
chrin front of one of them.The rule is that it never silently matches two different sequences, and everything else follows from it.
lengthvetoes rather than persuades: two sequences of equal length are not the same sequence, so nothing is ever matched because the lengths agree, and a match whose two lengths disagree is reported rather than taken — hg19’schrMand hg38’schrMare one name and two sequences, two bases apart. A name whose shape is not recognised is matched by its spelling and nothing else, which is what makes this safe on a metagenome:NODE_1_length_338947_cov_15.4is an opaque id from an independently assembled organism, and two of them agree by coincidence (§8.5).Xis the hard one, and the list settles it.Xis the tenth chromosome of S. cerevisiae and the sex chromosome of everything else, and the string cannot tell you which — so the decision is made from the whole file: one that also holdsIXandXIis counting in Roman, and itsXis ten; one whose numerals stop atVis C. elegans, and has no chromosome ten to confuse it with. Which is also why the reading travels: a companion panel is told how the lead counts, because a name cannot carry that and a guess would draw chromosome ten under the labelX.Every match says which rule reached it — the
chrprefix, leading zeros, Roman numerals, an organelle spelled another way, the accession — and a panel that answered a name it does not itself write prints both spellings under its heading. A rename that is invisible is the one way this can be quietly wrong.And a good deal is deliberately not equated.
2Lis not2— a Drosophila arm is its own sequence — which is the same rule that keeps wheat’s1A,1Band1Dapart._random,_altand_fixstay, because a patch is not the chromosome it patches.LG1keeps itsLG.C,DandMare not Roman numerals here, because no genome has a hundredth chromosome andchrMneeds to stay the mitochondrion. Genome and MAG names are never matched at all.Two ranges of per-species knowledge are hard-coded and both are arithmetic: human’s
NC_000001–NC_000024and mouse’sNC_000067–NC_000087. There is no alias table to download, no assembly to declare and no network call — the three constraints (§1) rule all three out, and a rule that works on a genome nobody has published is worth more than a table of the ones somebody has.parseMetacoolerRangestays a faithful port ofmetacooler.ops.range: the conventions are consulted after both namespaces have been asked literally and the answer would otherwise have beenunknown-label, so every string a file does carry resolves exactly as it did, and a session of one file cannot tell any of this exists. SPECS §5.9. -
metacooler-run: the Analysis panel, from a shell. Fourteen tasks, every one of them goldened against cooltools, coolpuppy, pyBigWig and numpy — and reachable only by clicking. The moment somebody has two hundred files rather than one, clicking stops being the interface and they leave for cooltools, which is the right tool and a different set of definitions, so the numbers in the figure and the numbers in the table stop being the same numbers.So: the same kernels, the same writers, the same
#provenance header. Not a port —RUNNERSis imported and the job handed to it is built exactly as the analysis worker builds one, so a TSV produced in a batch and a TSV produced by clicking are byte-identical given the same inputs. If the two ever disagree, one of them is wrong and there is a golden that says which.metacooler-run tasksmetacooler-run insulation *.mcool --window chrII --out results/metacooler-run replay session.metacooler.json --over 'data/*.hic'replayis the half that matters. It takes the session file the Save button and the Share link already produce, and re-runs the analyses recorded in it — with the parameters recorded in it — over any number of other files. That closes a loop the app was missing at both ends: parameters get chosen the way parameters should be, by looking at a picture and adjusting until it is right, and then applied to the other hundred and ninety-nine with no retyping. A transcription error in a batch of two hundred is two hundred wrong numbers that look right.Built by
npm run build:cli— a fourth invocation of the same bundler over the same source, never an edit to the first. Node needs no loader for it and the CLI needs no dependency beyond the app’s own.Three refusals, each for a reason. No remote files: the reader uses synchronous range requests, which exist in a worker and not in Node, and
curlis better at the missing half than anything this could contain. An unknown--flagis an error, never ignored — a typo in a script that runs for an hour must not quietly use the default. And a task’s own parameters win over the CLI’s flags where the names collide, which is--balanceon six tasks: reserving the name first would have set the framing and left the task at its default, which is exactly the failure the unknown-flag error exists to prevent. The framing’s window is--windowrather than--regionfor the same reason — two tasks already have aregionparameter meaning which sequence. -
A deck’s results stop overwriting each other, and become one figure. Three things, and the first was a defect rather than a gap.
runAllhas computed one result per panel since the deck existed, and the runner stored them keyed by task alone — so the fourth library overwrote the third overwrote the second. What a batch of four left behind was four synthetic tracks and one plot: the tracks were filed per map and the figures were not. The same map also serves as the composite cache (§18.2), and a recipe is params plus framing, and a batch shares one framing across every target — it is taken once, when Run is pressed. So the reuse test could not see that cached eigenvectors were measured on a different file, and a saddle run over a deck drew panel B’s compartments out of panel A’s numbers. Silently, and only for the composites. Results are keyed by map and task now, which fixes both.With four results to show, the drawer grew tabs: one per map, arrows either side, and
all mapslast. Both navigations, because they answer different questions — a reader checking whether panel three looks like panel two presses next repeatedly without aiming, and a reader who wants the fourth library clicks its name.all mapsis the overlay: every map’s curves on one set of axes, the series named by the map. This is not §18.9’s refused differential and the distinction is exact: that declined a subtraction, on the grounds it is a worse version of a picture the app can already make. Four P(s) curves on one axis is not a subtraction, derives nothing, and is a picture the app could not make — the single most-drawn figure in the literature and the reason people leave a browser. The machinery was all here and unconnected:CurveOutputhas been multi-series with a legend and band drawing since it was written.A matrix and a table stay per map, and the tab says which rather than showing three of a task’s five outputs and letting the reader assume that is all there are. A pileup averaged over four libraries would be a fifth thing nobody asked for.
-
The figures answer questions now. Log toggles per axis, a series picker (alt-click a chip to isolate it), a hover readout, a drag across the plot to zoom an x range, double-click to reset — and, for a pileup or a saddle, its colour range and colormap.
No chart library, and no second renderer. The whole technique is that an interactive figure is a plain figure of a derived output: a log axis is a
CurveOutputwithxScale: 'log'handed to the samedrawCurve, a hidden series is one with fewer entries, a zoom is one whose series are clipped to the dragged range. So the PNG is still a redraw and the PDF is still true vector — and, the property worth the most, the export is what is on screen. A reader who zooms into the first decade of a P(s) and presses PDF gets that picture, at vector resolution, with its axes relabelled for the range it shows. A canvas transform would have given them the whole curve behind a clipping rectangle. -
Share: the whole session as a link. §5.5 says the URL carries what is being looked at and the session file says the rest; both stay true, and neither serves the case between them — somebody wants to send a colleague what they are looking at, right now. A link carries none of the work, and a file has to be attached, downloaded and dropped, by which time the moment has gone.
So the session — the view, the tracks with their colours and limits, the marks, the bookmarks — is deflated and base64url-encoded into the fragment. That choice is the privacy property, not an implementation detail: a fragment is never sent on navigation, so what somebody is looking at never reaches the host serving the page. Nothing holds the link, so it cannot expire, cannot be taken down, works offline and works in the desktop build.
What it costs is length, and length is the cheapest of those to give up — a few hundred characters for a view, a few thousand for a workspace. The dialog says the number when it gets large rather than guessing on the reader’s behalf: they know where they are about to paste it and the app does not.
A third-party shortener is offered beside it, never instead of it, and is named in the button rather than hidden behind a verb, because it breaks two of those properties: the link starts depending on somebody else’s service, and the address of the file being viewed leaves the browser. It is refused outright for a session over a local file — the long link is still worth having there, the recipient getting the tracks and the marks and being asked for the file, but shortening it would post a filename to a stranger in exchange for nothing. If the service cannot be reached the long link is untouched and stays on screen.
-
bedGraph and wig open as tracks, which closes an asymmetry that was invisible and slightly embarrassing: the app wrote bedGraph — every analysis result offers the button — and could not read one back. A four-column bedGraph is BED-shaped, the sniffer knew three formats, and the value column landed in
name, so the file came back as a lane of featureless grey boxes with no signal in it. Silently, which is the worst of the three ways that could have gone.A bedGraph is a bigWig written out, so it becomes the same kind of thing: values held in the annotations worker and sampled per view, which is what gives it a range, a ceiling, an autoscale, a colour chip and the ⌘-scroll without one line of new interface. What it cannot borrow is the bigWig’s on-disk zoom levels, so the summary for a window — mean, min and max, the mean weighted by covered bases rather than by interval count — is computed when the window is asked for. wig arrives with it, both step styles, its 1-based inclusive coordinates converted at the parser.
The new sniffer runs before the feature sniffer and is deliberately the stricter of the two: exactly four columns, numeric on every row it inspects, or a declaration line saying so. A rule loose enough to capture a real BED4 would turn every peak set into a signal track of NaN, which is a worse bug than the one being fixed, so the suite tests the refusals as hard as the acceptances.
-
A tabix sidecar makes a feature file a bounded read. Every other read here already was — HDF5 through range requests, a bigWig by zoom level, a
.hicby the blocks its rectangle covers — and then a GTF was downloaded whole. The demo file is 9.5 MB; GENCODE is five times that.Where a
.gzis BGZF and a.tbiis beside it — dropped alongside, or at the conventional<url>.tbi— only the byte spans holding the sequences this map is about are fetched. Against a map of one chromosome that is about a twentieth of GENCODE. BGZF and the index are read here rather than depended on, for the reasonhic/format.tsgives at length: the byte layer, its LRU, its request accounting and its S3 signature belong toByteSource, and a parser that fetches for itself sits outside all three.The fallbacks are the feature. A plain GTF still works, a bgzipped one with no index still works, and an index that is absent, corrupt, truncated or for another assembly falls back to the whole-file read rather than refusing the track — an index that can prevent a file from opening is worse than no index. A sidecar is only asked for when the file is a
.gz; a span set covering more than four fifths of the file is abandoned as no saving; and sequence names are matched through thechrprefix in both directions, because Ensembl writes1where UCSC writeschr1and reading the whole file over that would be the index quietly not working.One thing worth knowing before touching it: the fetch has to over-ask by up to 64 KiB, because a span ends at the start of the last block holding a record and a block’s length is not knowable without reading its header. The decode is bounded separately. Without that, a one-sequence read on a densely packed file hands back the several sequences that happened to follow it — which the suite caught, and which reads as the index working.
-
.hicfiles open, and nothing downstream can tell. Versions 7, 8 and 9, local and remote, at every base-pair resolution the file carries. This is where the published record is: most mammalian Hi-C on 4DN, ENCODE and GEO is distributed as.hicand as nothing else, and until now this viewer could not be pointed at any of it.It costs no constraint. A
.hicis block-indexed with a master index at a known offset, so a region is a bounded range read through the sameByteSourceas everything else — the same 1 MiB LRU, the same §12 request accounting, the same S3 signature for a private bucket. A block is not a tile: nothing is composited, nothing is cached to be assembled, and a read hands up one whole bounded matrix in COO exactly as a cooler’s does. If anything the format suits §2 better than HDF5 does, its index being two-dimensional — so the off-diagonal rectangle this application exists to draw costs the blocks it covers rather than a whole row band across the genome.The integration is two modules and a seam rather than a second reader.
hic/handle.tsanswers the questions a cooler’s HDF5 groups answer — a chrom table, an offset array, root attributes, a weight column — andmetacooler/store.tsdraws aPixelStoreseam at the operation rather than at the dataset, because the one thing a.hicgenuinely cannot pretend to have is a globally sorted pixel table. Above those two,MetaCooleris unchanged and everylayout.kind === 'metacool'branch in the application falls the right way untouched. The resolution ladder is spelledfile.hic::/resolutions/5000, so the entry picker, the URL hash, the refine ladder and a deck descending to its lead’s rung all work with no code of their own.Three translations, each argued in
SPECS.md§3.6: the syntheticAllchromosome is dropped by name (never by index — the master index is keyed on the file’s own numbering); normalisation vectors becomebins/weight, reciprocated into cooler’s multiply-by convention, with the chosen one named in the info panel rather than implied; and fragment-resolution matrices are excluded, because a fragment grid is variable bins under another name and §3.3 excludes those from the whole application.Checked three ways, and the third is the one that earned its place. Fixtures whose contents are known exactly, across every encoding the format allows — v7/v8/v9, short and float values, short and int positions, list-of-rows and dense blocks, grid and diagonal block numbering. Then
hic-straw, the reference implementation, as a devDependency oracle over all of them. ThenMetaCooleritself, asserting the ordinary cooler surface answers identically. The oracle immediately found what a self-consistent reader and writer cannot: they had agreed with each other that the format is big-endian, and it is little-endian. It also caught a v9 straddle clause that silently lost every near-diagonal block while leaving the distant ones correct — a map with rectangular holes exactly where the signal is. -
Bookmarks: places worth coming back to, in a card under the hand-drawn marks. A bookmark is a region and a bin size, and carrying the second one separately is the point — on a contact map where and how closely are two questions, a loop found at 1 kb is not visible at 20 kb, and coordinates without the grain give back a picture the note was never made against. Nothing else is in one: not the zoom, not the colour scale, not which way the map was turned. Those are how the picture happened to be drawn, and a bookmark that restored them would be a saved screenshot rather than a saved place.
Going to one opens the whole sequence, standing on the location — and that is not what typing
chrI:1,200,000-1,260,000into the region box does. That cuts a block at those coordinates, so the edges of the picture are the edges of the data and panning off them shows paper. A saved place openschrIand puts the location in a newfocuskey, which is a window over a block read whole: the first thing anybody does on arriving somewhere — look at what is either side of it — works. It lands on the diagonal, because a locus is one coordinate and a contact map takes two, and both presentations are served from the same window: a square map takes it on both axes, a turned one along the genomic axis with the other following. Arriving at a second place on the same chromosome is a zoom rather than a navigation, so walking a list of forty peaks costs one read of the chromosome and not forty.focusis in the hash, so a bookmark is a link. It is a one-shot initial condition rather than a live position — pan and zoom are deliberately not shareable state, and a URL that rewrote itself on every wheel notch would make the back button a scrollback — so the link goes on naming the place it was made for however far its recipient then wanders.Three ways in, because they are three moments. A
bookmarkbutton besidegoon the title line saves the window on screen, for the place you have just found and do not want to retype; the round trip to a sidebar to copy coordinates you are already looking at was the whole cost of not having it. The card takes typed coordinates, in every spelling this app’s own readout and tooltips produce — separators,kb/Mb,..from an IGV or Ensembl URL, and a bare position for a loop anchor. And it imports BED, which is what anybody who has called peaks or loops already has in hand: the first three columns and the name, so a narrowPeak, a BEDPE’s first mate and a headed table from R all import without a format question being asked. It says how many arrived and how many lines had nothing in them, because an import that silently keeps four rows out of forty has told the reader nothing.Every refusal is said: a coordinate that will not parse, a picture that is not at one place, a file with no regions in it, a place already saved. All four are a press that would otherwise appear to do nothing — including pressing
bookmarktwice, which is what anybody does when they cannot remember whether the first press worked, and which answers “that place is already bookmarked” rather than growing a second identical row.They are kept in per-file session memory beside the marks, so they survive a change of resolution, a switch of view, a deck opening and closing, and a reload; and they are carried in an exported session, because a list of loci walked one after another is work, and it is the one part of a workspace a recipient can act on without the bytes.
-
A square map is resized by dragging any of its four edges, as a band has been by three of its own. The handles are the same bars, faint until they are approached and lit under the hand, and a double-click on any of them — or
Homewith one focused, since they answer the keyboard too — gives back as much room as there is.It is one number, and every edge sets it. A band has a width and a depth because it is cut to fill them; a square block has no shape to choose, so a box of any other shape is paper down two sides of the picture. The two ends move it at twice the hand, because the box is centred in its column and the edge under the pointer has to stay under it; the top and bottom move it at once, and the picture grows downward from the stack above it rather than floating in a reserved column with its tracks detached.
The size is a preference about the window, like the band’s box and the theme: it is remembered in
localStorage, it is shared by every map on screen so a deck stays one shape, and it survives the navigation that rebuilds the panel — which is the gesture anybody who has just sized a map is about to make. It is clamped to the room rather than stored as what was drawn, so a size chosen in a tall window comes back in full when that window comes back, and a narrower one draws as much of it as fits. It works in a deck too, from any panel: the number is shared, so one drag resizes every map on screen and they stay comparable. -
The whole viewer as one HTML file you double-click.
npm run build:standaloneemits a singledist-standalone/index.html— every script, stylesheet, worker and icon inlined, h5wasm included — and it is attached to every release asmetacooler-viewer-<tag>.html, so nobody has to build it. No server, no install, no npm, no clone. Put it on a USB stick or in an attachment; the reader opens it and drops a file on it.Three situations the site, the PWA and the installers between them do not cover: an air-gapped or HPC machine with no route to the website, a reviewer who will not install an unsigned binary, and a laptop holding unpublished patient-derived data whose owner would rather not take anybody’s word about what leaves it. Nothing is uploaded from any build of this app, and §11 has said so from the start — but this is the build where that is not a claim. There is no origin to upload to.
Four things a
file://page cannot do, each measured in Chromium before a line of the config was written, and each absolute rather than degraded: it cannot construct a module worker, cannot fetch a sibling.js, cannot reach the network at all, and cannot register a service worker. So the workers are classic ones over blob URLs (?worker&inline,worker.format: 'iife'), the whole graph is one chunk (inlineDynamicImports), and the interface is honest about the third: remote URLs, the published examples and the version picker are absent in this build rather than present and failing. The fourth needed no change —pwa.tsalready treats a refused registration as “this page has no cache”.The one thing that turned out not to be a problem is the largest: h5wasm ships its wasm base64-inlined in its own JavaScript, so there is no
.wasmto fetch and nothing to work around. It is also why the file is about 11 MB, four fifths of it that runtime, appearing twice because two workers need it and afile://page has no shared chunk to give them. It gzips to about a fifth of that in transit; on open it is roughly a second of parsing with no HTTP cache behind it.scripts/standalone-shape.mjsstates the shape as rules the build enforces and a test holds, because every one of these failures is silent: a page that kept a<script src>paints its shell and then stops, and one that kept the module worker format looks completely normal until a file is dropped on it. One file, within a size band, no external reference, no root-absolute path, no{type:"module"}worker.scripts/smoke-standalone.mjsthen drives a real Chromium at a realfile://URL and opens a real.hicthrough a real worker, because every one of those rules can pass on a page that does not run. -
A saved tab records which file it was made from. Name, size and modification time, for a local file. It reads as bookkeeping and it closes a real hole: a tab names its file by URI, that URI is the bare filename for a local file, and opening one goes through a grant store keyed on exactly that name. So a workspace received from a colleague and naming
rep1.coolopened this browser’srep1.cool— somebody else’s data under the sender’s marks, bookmarks and colour limits, with nothing on screen disagreeing.It informs and never vetoes. A re-run pipeline overwriting a file somebody is reading is a legitimate thing to do, and the app cannot tell that from a different file on another machine — so a mismatch is said once and the session opens. Absent for a remote file, whose identity is its URL, and stripped from shared links, where it would be metadata about a private disk.
-
A saved file from a newer build was read as though it were current. The format carries a version, and
CONFIG_VERSION’s own docstring says the number is bumped only when a field changes meaning — so the single situation it exists for was the one situation nothing acted on. A file whosevminmeant a percentile would have been drawn as a log10 value, with every label on screen agreeing.It is now refused, and there is deliberately no open anyway: a version bump cannot say which field moved, so the button would be an offer to be silently wrong. When a bump does happen the honest fix is a migration on that line rather than a way past it, and the comment says so.
Refusing needed a reason rather than a
null, because two of the three sentences a reader used to get were false. A file from a newer build landed them on “is a JSON file but not a saved workspace”, and a link from one on “may have been cut short in transit” — both sending somebody to inspect the file, or their network, when what is actually wrong is the build they are holding. All three roads now say the same true thing, naming both format numbers and the build the file came from:figure3.metacooler.jsonwas saved by a newer version of the viewer — (meta)cooler 9.9.9, in workspace format 2. This build reads format 1. Update the viewer, or ask for the session to be saved again from this one.A shared link also now separates did not survive the journey from arrived intact and this build will not read it. Only the first is the link’s fault, and telling a recipient the second was truncated sends them back to the sender for a fresh copy that will fail identically.
-
A workspace published beside its data broke the moment it named a deck.
resolveConfigpoints a tab’s relative file names at wherever the file it came in was found — that is what makesdemo/yeast/a folder anyone can copy to a bucket, to a colleague’s bucket, or to a laptop. It resolved the map, the compared file and the tracks, and it had never been taught the three fields a tab grew when decks, lanes and local companions started travelling. So a published workspace naming a companionrep2.coolhanded the deck a bare name, which was fetched against the site root — a 404 on the website, andapp://viewer/rep2.coolin the desktop build. It read as a broken panel rather than as an unresolved name.state.mapsnow resolves through the deck’s own parser rather than a second|splitter written beside it: the rule that a raw pipe separates and a%7Cinside a member is data lives in one place, and a second copy of it fails by resolving to a different number of maps than were written.lanes[].uriresolves through the same call, and that pairing is load-bearing. A lane names the file its companion was showing and is matched by string equality, so resolving one and not the other would put every placed track back on the first map — a perfectly legal lane, drawn without complaint, and not where the sender put it. The test asserts the two against each other rather than against a literal, so it cannot pass with both halves wrong.Two fields are now dropped rather than resolved, and that is the more interesting half. A local compared file and a deck’s local companions are bare filenames from the sender’s own disk. Left alone they reach the grant lookup, which is keyed on the bare name and whose guard compares only the name — so a recipient who had ever granted their own
rep2.coolwould open their file in the sender’s slot, with every label on screen agreeing with them. Resolving them instead would mint a URL for a file that was never published. Dropping them costs only a label: the~placeholder still holds the slot, so the count, the order and the drop rectangle all survive.A local track is kept, and the rule that decides both is now written down: a private name is stripped only where the structure survives without it. A dropped companion costs a label; a dropped track costs the row, and a recipient silently receiving seven tracks where eight were sent is the exact failure the placeholder exists to prevent.
-
Which score the cells were drawn from did not survive a link, a reload or the back button.
scorehas been inKEY_ORDERsince the field existed, so every hash the app wrote carried it — andparseViewStatenever read it back. The round tripstate/ViewState.tsrequires of itself in its own header was broken for exactly one of its twenty-two keys, and silently, because what a droppedscorelands on iscount: a legal picture of the same region, in the same colours, with nothing on screen saying the quantity had changed.Three things it cost, none of which looked like the same bug. A shared link naming
balancedorICEopened on raw counts for the recipient. A restored tab did the same, because the tab strip stores that very string (app/sessions.ts). And the back button did it within a session:readHashre-parses on every popstate and compares by serialising, so it saw a difference, adopted the parsed state, and redrew a different quantity — press Back after a bookmark and the map changed meaning.The parser now reads it, as an open string like
colormaprather than against a closed list, because which/pixelscolumns exist is a fact about the file and not about this build; an id this file has no column for is repaired byscoreFor, exactly as a link naming a missing genome is. The round-trip test gained the case, and a second one that would have caught this on the day it was written: a state with every key set at once. Each of the twelve cases there is a picture somebody looks at, which is why none of them covered a key no case happened to use. -
An exported session forgot how its deck was arranged.
deck:starredanddeck:columnsare per-file session memory like the ticked sequences and the table filters beside them, and they were the two scopesbuildConfigwalked straight past.state.mapssays which maps a session holds, so those came back; the arrangement around them did not. A deck of six exported at three columns reopened at whatever the importing window computed for itself, and the Analysis column reopened pointed at the lead rather than at the map the sender had starred — both silently, because both are legal decks.Nothing is validated on the way in, deliberately:
PanelDeckalready refuses a column count it cannot use and already moves a star naming a slot it did not build back onto the lead. A second copy of those rules in the config reader would be a second place to keep them in step. -
Shift stopped sticking the readout to the diagonal as soon as the pointer moved. Shift is the map’s on-diagonal modifier — a Shift-drag names a stretch and opens it there — and holding it snaps the crosshair and the coordinates onto the diagonal so the reader can see which stretch that would be. Pressing the key snapped them; the first pixel of movement put them back, because the key press asked for Shift and the pointer move asked for Option-with-Shift. That is the one shape of bug a reader cannot report: it worked until they moved. Both now ask the same question, and a key event reconstructs the state of the modifier it did not carry rather than assuming it is up.
-
A turned map’s card went on describing a cell while the pointer was naming a place. With a modifier down the crosshair stands up as a single vertical line — the edge a band would begin on — and the card beside it still printed the two loci under the pointer and the distance between them, which is a fact about the cell and not about what the next drag would select. Held, the readout now names the position along the bottom and nothing else: the midpoint of the pair, which is what a turned column stands over and the same number the ruler is printing. Let go, it is a card about a cell again.
-
An arrowhead sat on a corner of the very cell it pointed at. The glyph is stood off along its own 45° axis, which puts it four pixels from the mark’s anchor — and the anchor is the cell’s centre, so the gap cleared the centre and not the corner. At any zoom where a bin is a handful of pixels, the tip covered the last row and column of the cell the reader was being asked to look at, which is the one thing an overlay here is not allowed to do. The whole glyph now stands a further pixel out, square to the axes: right and up in the upper triangle, left and down in the lower. Turned, where it stands straight over the dot, the extra pixel is vertical only — a sideways nudge there is a nudge to a different separation, and so to a different pair of loci.
-
Publishing the demos switched private buckets off, and the error blamed the wrong setting. An S3 CORS policy is bucket-wide — the rules have no prefix condition — and
put-bucket-corsreplaces the whole document rather than adding to it.scripts/publish-demo.shwrote one rule, correct for public data and namingRangeinAllowedHeaders, so every run wiped whatever allowed signed requests: from then on the landing page and every pasted demo URL worked exactly as before, and no private prefix could be read or browsed from any origin. The policy now carries both rules, and the verify step preflights the signed headers rather than only the unsigned read — the check that would have caught this, and the one nothing above it can, since every other call it makes is answered by the public rule.The message shown when the preflight fails sent the reader somewhere else entirely: it said the policy had to name the page in
AllowedOriginsbecause*“is not enough for a credentialed request”. A credentialed request is one carrying cookies or a client certificate; this app setsAuthorizationfrom script, sends no cookies, and a wildcard origin answers its preflight perfectly well. So the advice was to narrow a list that was not the problem, which changes nothing and leaves the one wrong line —AllowedHeaders, missingAuthorization,x-amz-dateandx-amz-content-sha256— in place. It now names that line, and says that a bucket serving the demos correctly is expected to fail this way.docs/hosting.mdcarried the same claim and no longer does. -
A batch ran on fewer maps than its own button counted. Run on all N maps over a freshly opened deck ran on the ones that happened to be ready and left the rest behind — and said nothing, because a result that never arrives looks exactly like a task that had nothing to report. Pressing again worked, which made a question about timing look like a question about the file.
A companion opens to draw a picture: the two tables an analysis needs are a further pair of reads it makes only when it is starred or batched, so an eight-panel deck does not fetch sixteen tables to show eight rectangles. The batch took the list of maps that could answer at that instant. It now waits for the ones still opening — every panel at once, since these are two small reads against workers that already exist — and the button counts every cell in the grid rather than only the ready ones, because every cell is going to be run on. A panel that genuinely cannot answer is still left out, and its own note already says why.
-
Dragging a lane on a companion moved the first map’s lane instead. The handle on a track’s lane is dragged onto another edge to move it there, and on a companion it moved the wrong picture: the track jumped to the edge on the lead, and the map the hand was actually over was left exactly as it was.
One call was doing two jobs.
placewrites the track’s default placement, which is what the row in the Tracks card shows and what a session with no deck means — and the lead’s lane follows it, because that control is the lead’s. Every gesture went through it, including the ones made on a picture that is not the lead. Now they go through a call that takes the map they were made on: the drag, the drop of a row onto an edge, the lane’s own right-click menu and the location control all move that map’s lane, and the axis still turns on the way across so the lane arrives reading the same way up as its neighbours — as a lane override on a companion, which is exactly whyflipis one of the two fields a lane may differ on.It bit computed tracks hardest, because a computed track opens on the left and wanting it under the map is the first thing anybody does with one.
-
Closing a deck left a companion’s tracks drawn nowhere. Putting the grid down is a statement about layout, and the two things a reader means by it are that the extra pictures should go and that the work they did on them should not. A coverage track computed on the third replicate simply disappeared — still imported, still in the list, drawn on no map — which reads as the close having deleted it.
A departing map’s lanes now come to the lead, at the edge they were on, and the reader gets them back where they were the moment a map opens that file again. Keyed on the file, never on the map: a map id is a slot minted per deck and a deck closed and reopened mints new ones, so the id a lane was parked under would never be seen again — what a reader means by “that map” is the file it was showing. A track the reader had deliberately put on both the lead and a companion is not rearranged: the lead’s lane is not going anywhere, and moving it to wherever the departing copy sat would be the close changing something nobody asked it to.
-
The score sat last in a companion’s corner menu and first in the Map card. The same five fields in two orders, on two maps of one deck. It leads both now, which is where it belongs: what the numbers are is the question the transform, the ramp and the clip all answer about.
-
A computed track had no way to say which map it belongs to. §18.5’s whole claim is that a per-bin result is a track — same colour chip, same name, same limits, same four edges — and the Synthetic card was the counter-example: it carried the colour, the name, the eye and the exports, and nothing at all about where the lane is drawn. The Tracks card’s own location control is now on those rows too, from one module rather than two, so a computed lane and a loaded one are moved by one gesture: four edges, a row per map once a deck has more than one, and every map / no map beside them.
-
A task run across a deck skipped the file that spells its chromosomes differently. Three
.coolfiles and a.hicbeside them, Run pressed on compartments: three tracks and three sets of A/B intervals came back, and the.hicproduced nothing at all.A batch (§18.2) runs one set of parameters against every panel, and those parameters were filled in against the starred map — so the ticked sequence list is that map’s spellings, a typed region is, and a loaded BED was renamed into the session’s on the way in. The worker then matched them by string.
MetaCooler.contigIdis a faithful port of cooler’s own lookup and knows only what the file writes, so on the panel that writes1where the others writechr1every name matched nothing, and the run ended on none of the ticked sequences has any bins in it — true, and not the reason.Names that arrive from outside the file now go through the same door §5.9 gives every other holder of a name list: the literal answer first, the conventions only where that would otherwise be a refusal, and the naming context of the list the name came out of carried with it — a ticked list is the source file’s own spellings, so it is what says whether its numerals are Roman. Compartments, insulation, P(s) and APA all read through it, so a region, a ticked sequence and a BED anchor each resolve on every panel of a deck.
And a short run now says why it was short. A sequence a file has not got is not an error — a deck holds different assemblies on purpose — but the ones that were left out are named, and a run that matched nothing says so and lists what the file does have, rather than reporting a clean result over nothing. The one thing still refused is a bare
Xwith no list behind it: it is the sex chromosome far more often than it is the tenth, and only the list it was written in can say which. -
Picking a locus for 4C zoomed the map out from under the pointer. Pick on map is a mode: while it is armed a plain drag names a stretch of genome for the form that asked, and nothing else. It zoomed as well, every time, and the reason is an ordering. The mode disarms itself the moment it has an answer — one pick per arming — and it disarms by calling straight back into the map to say the next drag zooms again. The map then consulted that setting on the line after it reported the band, so what it read was the mode the gesture had just left. The reading is now taken before the report, which is the rule the same gesture on a lane stack was already following.
-
A turned map’s echo marked a place no track has anything at. Hovering one map of a deck leaves a tick in the tracks beside every other map, and under a turn that tick was drawn at the column the echoed cell sits in — which is the midpoint between the two places the contact is between, and a coordinate the profile below has nothing at. It is two ticks now, one per anchor, and one where the two are the same place, which is exactly what the panel under the pointer was already doing with its own crosshair.
-
The desktop app’s header carried a broken image, in every release that had one.
publicDiris off for that target — deliberately, and it is one of the two guards that no dataset reaches the installer — so the one file frompublic/the renderer actually asks for,icon-192.png, was never emitted andmarkUrl()resolved to nothing.The pinned build had hit this first and solved it privately, with a nine-line plugin inside its own config; the desktop config was written afterwards by somebody who had no way to know there was a problem to solve. So the plugin is now
scripts/shell-icon.mjsand both configs call it — the same argumentpwa-tags.mjsmakes for itself in its own docstring, arrived at from the other direction. The next config to switchpublicDiroff inherits the fix rather than the bug.Emitted rather than inlined as a data URI, which is what the standalone target does: the desktop renderer has no size pressure,
img-src 'self'already covers it, and there is still exactly one drawing of the mark. -
Hand-drawn marks were lost on a change of resolution. Placing two marks on a map at 2 kb and picking 5 kb in the breadcrumb drew an unmarked map, in a single view and in a deck alike — and lost them in the worst way, invisibly: the marks stayed in storage under the old level’s own key, so they came back if the reader went back to 2 kb and stayed gone if they did not, which reads as the app deciding at random whether to keep your work.
A session’s memory of a file — the marks, the ticked sequences, a table’s filters, how a group of tracks is scaled — hung on the URI it was opened at, and for an
.mcoolthat URI names an entry:…/Ecoli.mcool::resolutions/2000. Changing resolution reopens the file at another entry, so it asked a key nothing had ever been written under. It is now keyed on the file, the path before::— the definitionreopeningSameFilealready uses, and the reason a change of resolution carries every field of the view state across. Everything under that key is the same kind of fact: a mark is a contig and a base pair (§8.1), a ticked sequence is a name, a filter is a phrase about rows, and none of them means anything different at another bin size. Two tabs on one file stay two workspaces, because the session id is the other half of the key. Tracks were the exception that hid the size of this — they are handed across the reopen live rather than recalled — which is why it surfaced as “my annotations are gone” and not as everything at once. -
A deck of square maps drew them at whatever size their canvases happened to be left at, with air between them. Two maps at 1 kb came up as 182-pixel pictures in 430-pixel cells; changing resolution in the breadcrumb moved them to 320 and still never reached the cell; and turning the map and back was the only thing that settled it — which is the shape of the report that named this bug.
A map’s box sits in a grid track sized from its own contents, because a map has to be able to give width back to the lane stacks beside it. Inside a deck its width was
min(100%, …), and a percentage with no definite containing block falls back to intrinsic sizing — which for a box whose only child is a canvas means the canvas’s own pixel size, a number derived from that same box a frame earlier. A change of resolution is a new canvas, so the circle settled somewhere new; a turn writes lengths, so it broke the circle. Both the lead’s rule and the companions’ fallback are now the size the panel measured, andtest/unit/deck-plot-css.test.tskeeps them numbers — the same fault had already been fixed once for a turned companion’s height. -
A turned map now takes the room it is given, whenever the room changes. A band’s rectangle is the one picture in this app whose size is a number the panel computes rather than a length the stylesheet sets, and only some of the roads into it redrew that number. So the band kept the width of the window it was first drawn in: folding two columns handed it three hundred pixels it never took, pressing the turn toggle drew it to a room measured before the fold, and a window resized around it left it where it was. All four roads — the shell folding a column, the view’s own resize observer for a window or a new lane, a turn arriving or leaving, and a deck relaying out — now end in one place that re-measures the room and redraws the box, and the band’s depth follows on the next frame as it always did. A width the reader dragged is still theirs: it is clamped to the room rather than grown into it, and a double-click on the handle gives back “as much as there is room for”.
-
The band on a long chromosome aliased while scrolling into it. Pooling caps each axis on its own, which is right for a genome pair — the two axes are two different sequences, and halving the short one to suit the long one loses detail nothing asked for. On a turned band the two axes are one measure: the rotation is isometric, so a raster pixel four cells across and two up is drawn as a rectangle where the geometry promises a square. E. coli at 1 kb turned is 9,281 × 4,039, which pooled to 4 × 2 — and then every decision downstream had one answer for two scales. At the crossover the x axis was magnified through a blur while y was still point-sampled; past it, nearest neighbour laid cells down two device pixels wide and one tall. Against structure that runs at 45° that is a cross-hatch of smeared diagonals over the whole band, which is what a reader saw as aliasing at the very zooms they had scrolled in to look closely.
A turned band now pools with one stride on both axes, so its raster pixels are square in matrix space, the filter question has one answer, and the pyramid level is the right one for both directions. It costs no memory (E. coli’s band is 4.2 M pixels where it was 4.7 M) and no detail worth having: the axis that loses resolution is the one that was already being drawn at three cells per device pixel, and zooming in re-reads the window at a finer grid anyway.
-
Marks could not be placed by pointing at a turned map, which is every map a region view opens.
pick on mapcollected nothing and the right-click Mark this point menu did not open at all — silently, both of them, because a click that places nothing looks exactly like a click that missed. Typing coordinates worked and the marks drew correctly once they existed, so the feature looked broken in the one presentation it is most useful in.One conversion is behind both gestures: a cell into the pair of loci a mark names, one per axis, which is what lets a single click serve a line, a crosshair and a loop. It read the loci off the axes the picture is drawn against, and on a turned map the row axis of that pair is deliberately absent — y there is distance from the diagonal, which is not a place, and x is counted in half bins. The cell is now taken back to the square it was rotated out of and read against that block’s own two axes, which is what the tooltip, the readout and the region-opening selection have always done. Clicking a dot in the band therefore marks the loop it is: the two ends of the contact, 10 kb apart at ten rows up, rather than nothing at all.
-
The three track tasks failed with “nothing is open” in a session that has no contact map. A metaplot, a track correlation and an interval overlap read the loaded tracks and never touch a matrix (§18.3) — the 1D browser session is exactly where a reader has the bigWigs and not the map, and the panel is offered there deliberately. Everything on the way was ready for it: the context is built without a file, the framing falls back to one over the tracks. The last line still assuming a matrix was the target a result is filed under, which came from the open file and so from nothing at all.
It now comes from the genome the tracks were loaded against, and a target with no file behind it is no longer a broken target: a chain that mounts nothing does not need something to mount. A map task that reaches one refuses in its own words — Diamond insulation reads a contact map, and this session is browsing a genome without one — instead of the queue’s old “nothing is open” for a session with a genome and nine tracks in it.
-
The map aliased near the diagonal on screen while the exported figure did not. A canvas filter is one bilinear tap: it reads a 2 × 2 neighbourhood per destination pixel, which is a filter down to a halving and a fiction past it. A whole chromosome at 1 kb is a 3,000-pixel raster drawn into 460, so seven cells of every eight were never read — and on a turned map, whose lattice is half a bin and shifts parity row by row, what beat against the pixel grid was a moiré of diagonal stripes across the band. The export was spared because 300 dpi is three times the density: the same tap had a fifth of the work to do.
The view now keeps halved copies of the raster and samples the one nearest the size it is drawing, so the filter is never asked to reduce by more than a half — which is the one reduction it does honestly. Levels are built on demand and dropped with the raster: a region view drawn a pixel per bin builds none, and no level is ever built for a frame that does not ask for it. Magnified pictures are untouched, still nearest-neighbour, still crisp to the bin.
Documentation of chromosomix v0.20.0 · built from 40ec1ec · source
