444b1eb7e7ec2938a4a9a6d3ed214179073ab6f7
max
Wed Sep 9 05:41:23 2026 -0700
Faceted composite: manual row reordering, group-by, saved UI state, and per-facet "only" links
The Fiber-seq compendium put 41 samples times six data types into one
faceted composite, which pushed on the parts of the page that were built
for a flat list of tracks. Changes here, all in the shared faceted
composite code rather than anything Fiber-seq specific:
Row order. Track order in the image follows the table, so the table now
lets you set that order by hand. Vendored DataTables RowReorder 1.5.1
adds a drag handle as the first column after the checkbox, enabled on
the "shown in the browser" tab where reordering means something. The
dragged order is remembered by sample name rather than by row number, so
it survives a metadata file whose contents have changed.
Group by. A container of six data types can be read two ways, so the
page offers both: group the image by sample, keeping a sample's six
tracks together, or by data type, putting all the accessibility tracks
next to each other. cartDump assigns the priorities and just swaps the
nesting of its two loops. trackDb sets the starting choice with
defaultGroupBy.
Saved state. Facets, per-column searches, sort column, page length,
which tab was open and the hand-dragged order go to localStorage keyed
by metadata id, so coming back to the page does not mean setting it all
up again.
Facet "only" links. A small "only" appears on hover behind each facet
value and narrows to just that one, instead of unticking the others by
hand.
Column descriptions. A metadata column heading can now carry a longer
explanation after a "|", shown behind an info icon on both the column
header and the facet heading.
Also: parseDataTypes() was returning its list reversed, since slPairAdd
prepends and nothing put it back, so the data type checkboxes and the
resulting subtrack order were backwards; the composite lifts itself out
of hide when the user touches anything on the page, which is what they
meant by touching it; the facet sidebar collapses when a table has no
facetable columns; and the label wording throughout says "samples" and
"in the browser" rather than "tracks" and "active".
The Methbase hg38 track gets labels for its three data types, which were
showing as the bare pipeline names hmr, levels and reads.
refs #36210
diff --git src/hg/htdocs/style/facetedComposite.css src/hg/htdocs/style/facetedComposite.css
index 204a090052a..3c15c545777 100644
--- src/hg/htdocs/style/facetedComposite.css
+++ src/hg/htdocs/style/facetedComposite.css
@@ -1,247 +1,335 @@
/* SPDX-License-Identifier: MIT; (c) 2025 Andrew D Smith (author) */
/* Loading indicator shown while metadata is fetched and the table is built */
#faceted-loading {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 0.75em;
padding: 3em 0;
color: #555;
}
#faceted-loading .faceted-spinner {
width: 36px;
height: 36px;
border: 4px solid #d3eaff; /* matches the table's light-blue accents */
border-top-color: #2b6cb0;
border-radius: 50%;
animation: faceted-spin 0.8s linear infinite;
}
@keyframes faceted-spin {
to { transform: rotate(360deg); }
}
+/* The data type checkboxes control what every selected sample contributes to
+ the image, which is a different kind of choice from the track settings above
+ them, so box them off. */
+#dataTypeSelector {
+ border: 1px solid black;
+ padding: 0.4em 0.6em;
+ margin-bottom: 0.6em;
+}
+
#container {
display: flex;
width: 100%;
gap: 1em;
align-items: flex-start;
box-sizing: border-box;
}
+/* Hidden by facetedComposite.js when no column produced any checkboxes. It is
+ a fixed-width, non-shrinking flex column, so an empty one would otherwise
+ leave 300px of blank space beside the table. */
+#filters.no-facets {
+ display: none;
+}
#filters {
display: flex;
flex-direction: column;
gap: 1em;
width: 300px;
flex-shrink: 0;
/* ADS: the lines below are for vertical scrolling if needed */
/* max-height: 80vh; */
/* overflow-y: auto; */
align-self: stretch;
box-sizing: border-box;
padding-right: 0.5em;
}
#filters > div {
display: flex;
flex-direction: column;
gap: 0.3em;
}
#filters label {
display: flex;
align-items: center;
gap: 0.3em;
cursor: pointer;
user-select: none;
}
#tableColumn {
flex: 1 1 auto;
min-width: 0;
box-sizing: border-box;
}
#theMetaDataTable_wrapper {
min-width: 0;
box-sizing: border-box;
}
/* Horizontal-scroll box around metadata tables: a too-wide table scrolls
internally in this instead of spilling off-screen. The toolbar (Show N, paging)
sits outside this box, so it stays visible at the wrapper's width.
The genome browser page wraps this content in an old-school
that sizes to its content. The "width: 0; min-width: 100%"
pair stops the metadata table from reporting the (very large) content width,
the outer table sticks to the page width, the metadata table's min-width:100% makes
it expand to fill that, and then then metadata's overflow results in a scroll bar.
*/
.table-xscroll {
width: 0;
min-width: 100%;
overflow-x: auto;
}
#theMetaDataTable {
width: 100% !important;
box-sizing: border-box;
box-shadow: 0 0 0 1px #ddd;
}
/* Light blue background for the table header (column titles + search row) */
#theMetaDataTable thead th,
#theMetaDataTable thead td {
background-color: lightblue;
}
#theMetaDataTable td:nth-child(n+2),
#theMetaDataTable th:nth-child(n+2) {
vertical-align: top;
/* white-space: nowrap; */
/* overflow: hidden; /* hide overflow */
min-width: 100px; /* Adjust width as needed */
}
/* Override Select 3.0's custom checkbox styling to use native appearance */
table.dataTable input.dt-select-checkbox {
appearance: auto;
width: auto;
height: auto;
}
table.dataTable input.dt-select-checkbox:checked::after,
table.dataTable input.dt-select-checkbox:indeterminate::after {
display: none;
}
#theMetaDataTable input.row-select {
/* additional checkbox styling */
}
table.dataTable {
width: 100%;
border-collapse: collapse;
table-layout: auto;
}
table.dataTable tbody tr:nth-child(odd) {
background-color: #f0f0f0;
}
table.dataTable tbody tr:nth-child(even) {
background-color: #ffffff;
}
table.dataTable tbody tr:hover {
background-color: #d3eaff; /* Light blue on hover */
}
.color-box {
display: inline-block;
width: 1em;
height: 1em;
vertical-align: middle;
/* background-color set dynamically in JS */
}
.facet-heading {
cursor: pointer;
user-select: none;
}
.facet-heading::after {
content: " ❯";
margin-left: 0.3em;
display: inline-block;
transition: transform 0.2s;
transform: rotate(90deg); /* points down = expanded */
}
.facet-heading.collapsed::after {
transform: rotate(0deg); /* points right = collapsed */
}
.facet-body {
display: flex;
flex-direction: column;
gap: 0.3em;
}
.facet-body.collapsed {
display: none;
}
/* "All / Selected" segmented tabs in the toolbar */
#selected-filter {
display: inline-flex;
align-items: stretch;
order: -1; /* sit at the left edge, ahead of the page-length dropdown */
}
/* Top toolbar: both the page-length dropdown and the "show only selected"
toggle live inside .dt-length (the toggle is appended there in
facetedComposite.js). Stretch the cell full width and push the two
children apart: toggle on the left (via order: -1 above), dropdown right. */
#theMetaDataTable_wrapper .dt-layout-start:has(.dt-length) {
flex: 1 1 auto;
}
.dt-length {
display: flex !important;
width: 100%;
align-items: center;
justify-content: space-between;
+ /* Three controls now share this row (the selection tabs, the page-length
+ dropdown and the grouping tabs) and their labels are long enough to
+ overflow a laptop-width window, so let them fall onto a second line
+ rather than run off the edge. */
+ flex-wrap: wrap;
+ row-gap: 0.4em;
}
/* Tab-like blocks for the "All / Selected" selection filter */
.filter-tab {
border: 1px solid #aaa;
background-color: #f0f0f0;
color: #333;
padding: 0.3em 0.9em;
font-size: 0.9em;
cursor: pointer;
user-select: none;
transition: background-color 0.15s;
}
.filter-tab:first-of-type {
border-radius: 4px 0 0 4px;
border-right: none; /* avoid a doubled border between the two tabs */
}
.filter-tab:last-of-type {
border-radius: 0 4px 4px 0;
}
.filter-tab:hover {
background-color: #e3e3e3;
}
.filter-tab.active {
background-color: lightblue; /* matches the table header band */
color: #000;
font-weight: bold;
}
+/* "only" link on a facet value: narrows that facet to the one value. Revealed
+ on hover, and on keyboard focus so it is reachable without a mouse. Uses
+ visibility rather than display so the row does not reflow when the pointer
+ arrives. margin-left:auto pushes it to the right edge of the flex label. */
+a.facet-only {
+ /* Sits right after the count, not pushed to the right edge: at the edge it
+ is far from the value it applies to and easy to miss. The label is a
+ flex row with a 0.3em gap, so this adds up to a small clear space. */
+ margin-left: 0.3em;
+ font-size: 0.85em;
+ white-space: nowrap;
+ visibility: hidden;
+}
+#filters label:hover a.facet-only,
+a.facet-only:focus {
+ visibility: visible;
+}
+
/* Active filter chips bar */
#active-filters {
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: flex-start;
text-align: left;
gap: 0.4em;
padding: 0.5em 0;
border-top: 1px solid #ddd;
margin-top: 0.75em;
width: 100%;
}
.filter-chip-group-label {
font-weight: bold;
font-size: 0.85em;
margin-left: 0.3em;
}
.filter-chip {
display: inline-flex;
align-items: center;
background-color: #e8f0fe;
border: 1px solid #c2d6f2;
border-radius: 12px;
padding: 0.15em 0.5em;
font-size: 0.85em;
white-space: nowrap;
}
.filter-chip .remove-chip {
background: none;
border: none;
cursor: pointer;
font-size: 1em;
line-height: 1;
padding: 0 0 0 0.25em;
color: #666;
}
.filter-chip .remove-chip:hover {
color: #c00;
}
#sortNote {
color: #555; /* same muted grey as the loading indicator */
padding-bottom: 0.6em;
border-bottom: 1px solid #ddd;
margin-bottom: 0.75em;
}
+
+/* "Group by" tabs, shown next to the All/Active tabs when the composite has
+ data types. Same look as .filter-tab, with room to breathe from the tabs
+ to its left and a label so the pair reads as a question. */
+#group-by {
+ display: inline-flex;
+ align-items: center;
+ gap: 0.4em;
+ margin-left: 1.2em;
+ flex: 0 0 auto; /* never squeeze the label onto more lines than needed */
+}
+#group-by > span {
+ color: #333;
+ font-size: 0.9em;
+}
+
+/* Drag handle column for manual row ordering, shown only on the "shown in the
+ browser" tab. Narrow, muted, and grabby, so it reads as a handle and not as
+ data. The width goes on the header too: a width on the cell alone is only a
+ suggestion, and the header text would otherwise set the column width. */
+th.dt-reorder, td.dt-reorder {
+ width: 4.5em;
+ text-align: center;
+ white-space: nowrap;
+}
+td.dt-reorder {
+ cursor: grab;
+ user-select: none;
+}
+td.dt-reorder:active {
+ cursor: grabbing;
+}
+svg.dt-grip {
+ height: 1.1em;
+ /* Black, not grey: the selected rows this column appears on are drawn on a
+ saturated blue band, and a grey handle disappeared into it. */
+ fill: #000;
+ vertical-align: middle;
+}
+td.dt-reorder:hover svg.dt-grip {
+ fill: #444;
+}
+/* RowReorder floats a clone of the row being dragged; keep it legible over
+ the striped table underneath. */
+table.dt-rowReorder-float {
+ background-color: #fff;
+ opacity: 0.9;
+ outline: 1px solid #aaa;
+}