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/cartDump/cartDump.c src/hg/cartDump/cartDump.c index af5b1e9ddc3..cbc7de35776 100644 --- src/hg/cartDump/cartDump.c +++ src/hg/cartDump/cartDump.c @@ -1,327 +1,356 @@ /* cartDump - Dump contents of cart. */ /* Copyright (C) 2014 The Regents of the University of California * See kent/LICENSE or http://genome.ucsc.edu/license/ for licensing information. */ #include "common.h" #include "linefile.h" #include "hash.h" #include "cheapcgi.h" #include "cart.h" #include "hdb.h" #include "jsHelper.h" #include "hui.h" #include "botDelay.h" /* for earlyBotCheck() function at the beginning of main() */ #define delayFraction 1.0 /* standard penalty for most CGIs */ static boolean issueBotWarning = FALSE; #define CART_DUMP_REMOVE_VAR "n/a" struct hash *oldVars = NULL; void handleFacets(struct cart *cart) /* Process track vis updates for a faceted composite. This uses information about * which data types (dt) and data elements (de) were on before and after the * user made UI changes. If data types are used by the track, they're usually * things like "reads" and "signals". Data elements are generally either sample * IDs (if data types are also used) or just the ends of track names. * * If the user adds a new data element, we turn on all associated tracks for the * active data types. If a user adds a new data type, we turn on all associated * tracks for the active data elements. But if a data element/type combo was already * checked, we don't re-enable that track because the user might have hidden it * manually. */ { char *mName = "cartDump.metaDataId"; if (!cgiVarExists(mName)) return; char *mdid = cgiOptionalString(mName); char mdid_de_was[1024], mdid_de_now[1024], mdid_dt_was[1024], mdid_dt_now[1024]; char mdid_sort[1024]; safef(mdid_de_was, sizeof(mdid_de_was), "%s.de_was", mdid); safef(mdid_de_now, sizeof(mdid_de_now), "%s.de_now", mdid); safef(mdid_dt_was, sizeof(mdid_dt_was), "%s.dt_was", mdid); safef(mdid_dt_now, sizeof(mdid_dt_now), "%s.dt_now", mdid); safef(mdid_sort, sizeof(mdid_sort), "%s.facetSortOrder", mdid); // Grab the lists of which de/dt elements were on before and after the user // changed settings around struct slName *de_was_list = cgiStringList(mdid_de_was); struct slName *de_now_list = cgiStringList(mdid_de_now); struct slName *dt_was_list = cgiStringList(mdid_dt_was); struct slName *dt_now_list = cgiStringList(mdid_dt_now); // For faster lookup struct hash *de_was_hash = hashFromSlNameList(de_was_list); struct hash *de_now_hash = hashFromSlNameList(de_now_list); struct hash *dt_was_hash = hashFromSlNameList(dt_was_list); struct hash *dt_now_hash = hashFromSlNameList(dt_now_list); // Check if we sent dt_was/now variables, indicating that this composite has data types boolean hasDataTypes = (dt_was_list != NULL || dt_now_list != NULL); char subtrackSetting[1024]; char prioritySetting[1024]; // Remember how the faceted table was sorted, so that the next visit to the track // UI page comes back in the same order. The value is a list of metadata field // names with directions ("field=+ field2=-"), mirroring the classic composite // "<track>.sortOrder" cart variable. It's opaque here - only the javascript can // turn field names into an ordering, since only it reads the metadata file. char *facetSortOrder = cgiOptionalString(mdid_sort); if (isNotEmpty(facetSortOrder)) cartSetString(cart, mdid_sort, facetSortOrder); else cartRemove(cart, mdid_sort); +// Which of the two dimensions is kept together in the image: "sample" puts a +// sample's data types side by side, "dataType" puts the same data type for +// every sample side by side. Remembered so the next visit to the track UI page +// comes back with the same button selected. Only two values are accepted, so a +// junk value falls back to grouping by sample rather than being stored. +char mdid_groupBy[1024]; +safef(mdid_groupBy, sizeof(mdid_groupBy), "%s.groupBy", mdid); +char *groupBy = cgiOptionalString(mdid_groupBy); +boolean groupByDataType = (isNotEmpty(groupBy) && sameString(groupBy, "dataType")); +if (isNotEmpty(groupBy) && (groupByDataType || sameString(groupBy, "sample"))) + cartSetString(cart, mdid_groupBy, groupBy); +else + cartRemove(cart, mdid_groupBy); + // Any ".priority" values left over from an earlier submission are stale, since the // loops below assign a fresh, dense 1..N to exactly the subtracks that are on now. // Clearing them first means the subtracks the user just turned off don't keep a // priority that would place them oddly if they're ever turned on again elsewhere. // Note that "<mdid>_*" can't match the composite's own "<mdid>.priority" - the // subtrack names are joined with an underscore rather than a dot. char priorityWild[1024]; safef(priorityWild, sizeof(priorityWild), "%s_*.priority", mdid); cartRemoveLike(cart, priorityWild); // A subtrack the user has dragged up or down in the hgTracks image carries an // "_imgOrd" value, and flatTracksCmp() in hgTracks/imageV2.c sorts on that before // it ever looks at priority - so without this the dragged position would win and // the sort established here would appear to be ignored. cartJustify() in cart.c // normally clears these whenever a ".priority" arrives as a CGI variable, but ours // are set with cartSetInt() below, well after cartJustify() has run for this // request, so we have to do it ourselves. This is what the wiggle sort in // hgTracks.c does when it establishes a new order for a composite. char imgOrdWild[1024]; safef(imgOrdWild, sizeof(imgOrdWild), "%s_*_imgOrd", mdid); cartRemoveLike(cart, imgOrdWild); if (hasDataTypes) { // Cross-product mode: tracks are identified by mdid_de_dt // Turn ON: (de_on x dt_now), (de_now x dt_on) // where de_on = de_now - de_was, dt_on = dt_now - dt_was for (struct slName *de = de_now_list; de != NULL; de = de->next) { boolean de_is_new = (hashLookup(de_was_hash, de->name) == NULL); for (struct slName *dt = dt_now_list; dt != NULL; dt = dt->next) { boolean dt_is_new = (hashLookup(dt_was_hash, dt->name) == NULL); if (de_is_new || dt_is_new) { safef(subtrackSetting, sizeof(subtrackSetting), "%s_%s_%s_sel", mdid, de->name, dt->name); cartSetString(cart, subtrackSetting, "1"); } } } // Turn OFF: (de_off x dt_was), (de_was x dt_off) // where de_off = de_was - de_now, dt_off = dt_was - dt_now for (struct slName *de = de_was_list; de != NULL; de = de->next) { boolean de_is_off = (hashLookup(de_now_hash, de->name) == NULL); for (struct slName *dt = dt_was_list; dt != NULL; dt = dt->next) { boolean dt_is_off = (hashLookup(dt_now_hash, dt->name) == NULL); if (de_is_off || dt_is_off) { safef(subtrackSetting, sizeof(subtrackSetting), "%s_%s_%s_sel", mdid, de->name, dt->name); cartSetString(cart, subtrackSetting, "0"); } } } // Set each shown subtrack's priority to match the order the data elements - // are currently sorted in the faceted table (de_now arrives in that order). - // Data elements are the outer loop so a sample's data-type subtracks stay - // contiguous, in the sample's sorted position. + // are currently sorted in the faceted table (de_now arrives in that order, + // and dt_now in the order the data types are declared in trackDb). + // Whichever list is the outer loop is the dimension that stays contiguous + // in the image: by default that is the data element, so a sample's data + // types sit together in the sample's sorted position. Grouping by data + // type swaps the nesting, putting the same data type for every sample + // together instead. int priority = 0; + if (groupByDataType) + { + for (struct slName *dt = dt_now_list; dt != NULL; dt = dt->next) for (struct slName *de = de_now_list; de != NULL; de = de->next) { + safef(prioritySetting, sizeof(prioritySetting), + "%s_%s_%s.priority", mdid, de->name, dt->name); + cartSetInt(cart, prioritySetting, ++priority); + } + } + else + { + for (struct slName *de = de_now_list; de != NULL; de = de->next) for (struct slName *dt = dt_now_list; dt != NULL; dt = dt->next) { safef(prioritySetting, sizeof(prioritySetting), "%s_%s_%s.priority", mdid, de->name, dt->name); cartSetInt(cart, prioritySetting, ++priority); } } } else { // Data elements only mode: tracks are identified by mdid_de // Turn ON: de_now - de_was for (struct slName *de = de_now_list; de != NULL; de = de->next) { if (hashLookup(de_was_hash, de->name) == NULL) { safef(subtrackSetting, sizeof(subtrackSetting), "%s_%s_sel", mdid, de->name); cartSetString(cart, subtrackSetting, "1"); } } // Turn OFF: de_was - de_now for (struct slName *de = de_was_list; de != NULL; de = de->next) { if (hashLookup(de_now_hash, de->name) == NULL) { safef(subtrackSetting, sizeof(subtrackSetting), "%s_%s_sel", mdid, de->name); cartSetString(cart, subtrackSetting, "0"); } } // Set each shown subtrack's priority to match the order the data elements // are currently sorted in the faceted table (de_now arrives in that order). int priority = 0; for (struct slName *de = de_now_list; de != NULL; de = de->next) { safef(prioritySetting, sizeof(prioritySetting), "%s_%s.priority", mdid, de->name); cartSetInt(cart, prioritySetting, ++priority); } } hashFree(&de_was_hash); hashFree(&de_now_hash); hashFree(&dt_was_hash); hashFree(&dt_now_hash); slFreeList(&de_was_list); slFreeList(&de_now_list); slFreeList(&dt_was_list); slFreeList(&dt_now_list); cartRemove(cart, mName); cartRemove(cart, mdid_de_was); cartRemove(cart, mdid_de_now); cartRemove(cart, mdid_dt_was); cartRemove(cart, mdid_dt_now); } void doMiddle(struct cart *cart) /* cartDump - Dump contents of cart. */ { #define MATCH_VAR "match" char *vName = "cartDump.varName"; char *vVal = "cartDump.newValue"; char *wildcard; boolean asTable = cartVarExists(cart,CART_DUMP_AS_TABLE); if (cgiVarExists("submit")) { char *varName = cgiOptionalString(vName); char *newValue = cgiOptionalString(vVal); if (isNotEmpty(varName) && isNotEmpty(newValue)) { varName = skipLeadingSpaces(varName); eraseTrailingSpaces(varName); if (sameString(newValue, CART_DUMP_REMOVE_VAR) || sameString(newValue, CART_VAR_EMPTY)) cartRemove(cart, varName); else cartSetString(cart, varName, newValue); } cartRemove(cart, vVal); cartRemove(cart, "submit"); } if (cgiVarExists("noDisplay")) { // update cart vars for a track, called by hgTracks.js and ajax.js // not useful to hackers, so there is no need to call bottleneck. char *trackName = cgiOptionalString("g"); if (trackName != NULL && hashNumEntries(oldVars) > 0) { char *db = cartString(cart, "db"); struct trackDb *tdb = hTrackDbForTrack(db, trackName); if (tdb != NULL && tdbIsComposite(tdb)) { struct lm *lm = lmInit(0); cartTdbTreeCleanupOverrides(tdb,cart,oldVars,lm); lmCleanup(&lm); } } handleFacets(cart); return; } // To discourage hacking, call bottleneck if (issueBotWarning) { char *ip = getenv("REMOTE_ADDR"); botDelayMessage(ip, botDelayMillis); } if (asTable) { jsIncludeFile("jquery.js",NULL); // required by utils.js jsIncludeFile("utils.js",NULL); jsIncludeFile("ajax.js",NULL); printf("<A HREF='../cgi-bin/cartDump?%s=[]'>Show as plain text.</a><BR>",CART_DUMP_AS_TABLE); printf("<FORM ACTION=\"../cgi-bin/cartDump\" METHOD=GET>\n"); cartSaveSession(cart); printf("<em>Variables can be altered by changing the values and then leaving the field (onchange event will use ajax).\n"); printf("Enter </em><B><code style='color:%s'>%s</code></B><em> or </em><B><code style='color:%s'>%s</code></B><em> to remove a variable.</em>", COLOR_DARKBLUE,CART_DUMP_REMOVE_VAR,COLOR_DARKBLUE,CART_VAR_EMPTY); printf("<BR><em>Add a variable named:</em> "); cgiMakeTextVar(vName, "", 12); printf(" <em>value:</em> "); cgiMakeTextVar(vVal, "", 24); printf(" "); cgiMakeButton("submit", "refresh"); // Says refresh but works as a submit. printf(" " "<a HREF='../cgi-bin/cartReset?destination=cartDump'><INPUT TYPE='button' VALUE='Reset the cart' style='color:%s;'></a>\n", COLOR_RED); printf("</FORM>\n"); } else { printf("<A HREF='../cgi-bin/cartDump?%s=1'>Show as updatable table.</a><BR>",CART_DUMP_AS_TABLE); } printf("<TT><PRE>"); wildcard = cgiOptionalString(MATCH_VAR); if (wildcard) cartDumpLike(cart, wildcard); else cartDump(cart); printf("</TT></PRE>"); if (!asTable) { printf("<FORM ACTION=\"../cgi-bin/cartDump\" METHOD=GET>\n"); cartSaveSession(cart); printf("<em>Add/alter a variable named:</em> "); cgiMakeTextVar(vName, cartUsualString(cart, vName, ""), 12); printf(" <em>new value</em> "); cgiMakeTextVar(vVal, "", 24); printf(" "); cgiMakeButton("submit", "submit"); printf("<BR>Put </em><B><code style='color:%s'>%s</code></B><em> in for the new value to clear a variable.</em>", COLOR_DARKBLUE,CART_DUMP_REMOVE_VAR); printf("</FORM>\n"); } printf("<P><em>Cookies passed to</em> %s:<BR>\n%s\n</P>\n", cgiServerNamePort(), getenv("HTTP_COOKIE")); } char *excludeVars[] = { "submit", "Submit", "noDisplay", MATCH_VAR, NULL }; int main(int argc, char *argv[]) /* Process command line. */ { long enteredMainTime = clock1000(); /* 0, 0, == use default 10 second for warning, 20 second for immediate exit */ issueBotWarning = earlyBotCheck(enteredMainTime, "cartDump", delayFraction, 0, 0, "html"); cgiSpoof(&argc, argv); oldVars = hashNew(10); cartHtmlShell("Cart Dump", doMiddle, hUserCookie(), excludeVars, oldVars); cgiExitTime("cartDump", enteredMainTime); return 0; }