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/hgTrackUi/hgTrackUi.c src/hg/hgTrackUi/hgTrackUi.c index cccc3d87ba5..c852a23a148 100644 --- src/hg/hgTrackUi/hgTrackUi.c +++ src/hg/hgTrackUi/hgTrackUi.c @@ -3049,30 +3049,35 @@ for (int i = 0; i < n_datatypes; i++) { // If it's just a name, use that for the name and the display title. // But if it's of the form <name|title>, then split them apart. char *dtParts[2]; int partCount = chopByCharRespectDoubleQuotes(datatypes[i], '|', dtParts, 2); char *name, *title; name = title = dtParts[0]; if (partCount > 1) title = dtParts[1]; slPairAdd(&list, name, cloneString(title)); } freeMem(tdbDataTypes); +// slPairAdd() prepends, so the list is backwards from the trackDb setting at +// this point. The order is what the javascript renders the data type +// checkboxes in, and what cartDump.c uses to order a sample's subtracks, so it +// has to match the order the track was written in. +slReverse(&list); return list; } unsigned long cartDbParseId(char *, char **); // ADS: avoid extra include static void facetedCompositeUi(struct trackDb *tdb) { /* ADS: How facetedComposite differs from other track types * - compositeTrack track setting is "faceted" * - Required fields in the 'settings' longblob for the trackDb entry: * - 'metaDataUrl': a non-blocked URL (can be server-local) with * metadata to generate the table. This might change to an existing metadata * setting in the future. * - 'dataTypes': the names of the data types, ordered and space separated. @@ -3323,60 +3328,80 @@ { boolean clean = TRUE; char *c = facetSortOrder; for ( ; *c != '\0'; c++) { if (!isalnum((unsigned char)*c) && *c != '_' && *c != '.' && *c != '-' && *c != '+' && *c != '=' && *c != ' ') { clean = FALSE; break; } } if (clean) jsonWriteString(jw, "facetSortOrder", facetSortOrder); } +// Whether a sample's subtracks are kept together in the image, or all the +// subtracks of one data type are. Only meaningful with data types, since +// without them a sample is a single track. The cart value wins over the +// trackDb default; both are checked against the two words we accept, so +// nothing unvalidated reaches the <script> block. +if (hasDataTypes) + { + char groupByVar[1024]; + safef(groupByVar, sizeof(groupByVar), "%s.groupBy", metaDataId); + char *groupBy = cartOptionalString(cart, groupByVar); + if (isEmpty(groupBy)) + groupBy = trackDbSetting(tdb, "defaultGroupBy"); + if (isNotEmpty(groupBy) + && (sameString(groupBy, "sample") || sameString(groupBy, "dataType"))) + jsonWriteString(jw, "groupBy", groupBy); + } if (isNotEmpty(subtrackUrls)) { struct slPair *pairs = slPairListFromString((char *)subtrackUrls, TRUE); if (pairs) { jsonWriteObjectStart(jw, "subtrackUrls"); for (struct slPair *p = pairs; p != NULL; p = p->next) { char *encoded = htmlEncode((char *)p->val); jsonWriteString(jw, p->name, encoded); freeMem(encoded); } jsonWriteObjectEnd(jw); } slPairFreeValsAndList(&pairs); } if (isNotEmpty(cartOptionalString(cart, "udcTimeout"))) jsonWriteBoolean(jw, "udcTimeout", TRUE); jsonWriteObjectEnd(jw); printf("<script id=\"app-data\" type=\"application/json\">%s</script>\n", jw->dy->string); jsonWriteFree(&jw); /* --- END embedded JSON data --- */ jsIncludeFile("dataTables-2.2.2.min.js", NULL); jsIncludeFile("dataTables.select-3.0.0.min.js", NULL); +// RowReorder 1.5.1 is the last release for the DataTables 2.x line; 2.0.0 +// requires DataTables 3. +jsIncludeFile("dataTables.rowReorder-1.5.1.min.js", NULL); jsIncludeFile("facetedComposite.js", NULL); webIncludeResourceFile("dataTables-2.2.2.min.css"); webIncludeResourceFile("dataTables.select-3.0.0.min.css"); +webIncludeResourceFile("dataTables.rowReorder-1.5.1.min.css"); webIncludeResourceFile("facetedComposite.css"); // cleanup slPairFreeValsAndList(&dataTypes); hashFree(&defaultOn); } static void myVariantsShareUi(struct trackDb *tdb) /* Render the inline share management section on hgTrackUi for a user's own * myVariants track. Emits HTML + calls into hui.js via jsInline to wire up * the create/list/revoke API calls. */ { char *userName = getUserName(); if (userName == NULL)