e37f19d638c858f249b6b3e84586b6a2a3ca4487
braney
  Thu Sep 17 09:53:04 2026 -0700
hgTracks: color a lifted chain track by normScore, the way a native one is

lfFromLiftedChain set grayIx to maxShade and took the raw chain score, so a
quickLifted chain track asked to color by score drew every chain in the darkest
shade, and the dense mode sort by score ordered them differently from the native
track.  chainLoadItems reads normScore out of the chain table and uses it for
both.

normScore is a column of the SQL chain table and is in no bigChain file, so only
the SQL side can do this, which is also the only side the native loaders do it
on.  chainLoadRange throws the column away and struct chain has nowhere to keep
it, so chainNormScoreHash reads it separately, once per source range, and only
when the track is coloring by score.  chainDbNormScoreAvailable only reads a
trackDb setting, so the column count is checked before the row is indexed.

Verified with a trackDb stanza lifting hg19 chainSelf onto hg38: the same 1846
items drew in 7 distinct colors before and 30 after.  Note a hub cannot reach
this path, because chain is not a hub track type, so it takes a stanza of ours
carrying quickLiftUrl.

Found in the v504 code review, refs #38349.

refs #38249

diff --git src/hg/hgTracks/chainTrack.c src/hg/hgTracks/chainTrack.c
index 5da706078b1..dd3ad391498 100644
--- src/hg/hgTracks/chainTrack.c
+++ src/hg/hgTracks/chainTrack.c
@@ -352,44 +352,55 @@
         MgFont *font, Color color, enum trackVisibility vis)
 /* Draw chained features. This loads up the simple features from
  * the chainLink table, calls linkedFeaturesDraw, and then
  * frees the simple features again. */
 {
 
 if (tg->items == NULL)		/*Exit Early if nothing to do */
     return;
 
 linkedFeaturesDraw(tg, seqStart, seqEnd, hvg, xOff, yOff, width,
     font, color, vis);
 }
 
 
 static struct linkedFeatures *lfFromLiftedChain(struct chain *chain, struct chain *lifted,
-    int qs, boolean doSnake)
+    int qs, boolean doSnake, double normScore)
 /* Build the drawing item for one chain that has been mapped onto the reference.  The
  * blocks come from the lifted alignment, so unlike the native loaders this one does not
- * leave the components for loadLinks to fetch. */
+ * leave the components for loadLinks to fetch.  normScore is the chain's normalized score
+ * when the track is coloring by score, or a negative number when it is not; the native
+ * loader uses that number for both the shade and the dense-mode sort, so this one has to
+ * as well. */
 {
 struct linkedFeatures *lf;
 char buf[16];
 
 AllocVar(lf);
 lf->start = lf->tallStart = lifted->tStart;
 lf->end = lf->tallEnd = lifted->tEnd;
 lf->qSize = lifted->qSize;
+if (normScore >= 0)
+    {
+    lf->grayIx = hGrayInRange(normScore, 0, 100, maxShade+1);
+    lf->score = normScore;
+    }
+else
+    {
     lf->grayIx = maxShade;
     lf->score = chain->score;
+    }
 lf->filterColor = -1;
 
 lf->orientation = (lifted->qStrand == '-') ? -1 : 1;
 
 int len = strlen(chain->qName) + 32;
 lf->name = needMem(len);
 if (!doSnake)
     // qs was worked out from the source chain, so print its strand, not the lifted one
     safef(lf->name, len, "%s %c %dk", chain->qName, chain->qStrand, qs/1000);
 else
     safef(lf->name, len, "%s", chain->qName);
 safef(buf, sizeof(buf), "%d", chain->id);
 lf->extra = cloneString(buf);
 
 struct simpleFeature *sfList = NULL, *sf;
@@ -407,89 +418,132 @@
         sf->qStart = lifted->qSize - b->qEnd;
         sf->qEnd = lifted->qSize - b->qStart;
         }
     else
         {
         sf->qStart = b->qStart;
         sf->qEnd = b->qEnd;
         }
     slAddHead(&sfList, sf);
     }
 slReverse(&sfList);
 lf->components = sfList;
 return lf;
 }
 
+static struct hash *chainNormScoreHash(char *db, char *table, struct quickLiftRange *range)
+/* chain id -> normScore, as a string, for the chains in one source range.  chainLoadRange
+ * throws the column away and struct chain has nowhere to keep it, so read it separately
+ * rather than color a lifted chain track differently from a native one.  Returns NULL
+ * when the table has no normScore column, which the trackDb setting only claims. */
+{
+struct hash *hash = NULL;
+struct sqlConnection *conn = hAllocConn(db);
+int rowOffset;
+struct sqlResult *sr = hRangeQuery(conn, table, range->chrom, range->start, range->end,
+    NULL, &rowOffset);
+if (sqlCountColumns(sr) > rowOffset + 11)
+    {
+    hash = newHash(8);
+    char **row;
+    while ((row = sqlNextRow(sr)) != NULL)
+        hashAdd(hash, row[rowOffset + 10], lmCloneString(hash->lm, row[rowOffset + 11]));
+    }
+sqlFreeResult(&sr);
+hFreeConn(&conn);
+return hash;
+}
+
 static void quickLiftChainLoadItems(struct track *tg)
 /* Load chains out of the assembly this track came from and map them onto the reference.
  * A chain is an alignment between that assembly and some other species, so lifting one
  * composes two alignments and leaves the user with chains between the assembly on screen
  * and that species. */
 {
 boolean doSnake = cartOrTdbBoolean(cart, tg->tdb, "doSnake" , FALSE);
 struct cartOptions *chainCart = (struct cartOptions *) tg->extraUiData;
 char *optionChrStr = cartOptionalStringClosestToHome(cart, tg->tdb, FALSE, "chromFilter");
 if (isNotEmpty(optionChrStr) && sameWord("All", skipLeadingSpaces(optionChrStr)))
     optionChrStr = NULL;
 
 char *liftDb = trackDbSetting(tg->tdb, "quickLiftDb");
 char *table = NULL;
 quickLiftResolveTable(tg->tdb, tg->table, &table, &liftDb);
 char *quickLiftFile = trackDbSetting(tg->tdb, "quickLiftUrl");
 
 char *chainFile = NULL, *linkFile = NULL;
 if (tg->isBigBed)
     {
     chainFile = trackDbSetting(tg->tdb, "bigDataUrl");
     // linkDataUrl when the track names its link file, which not every track does by the
     // name bigChainGetLinkFile would guess.  This is the choice loadLinks makes.
     linkFile = trackDbSetting(tg->tdb, "linkDataUrl");
     if (linkFile == NULL)
         linkFile = bigChainGetLinkFile(chainFile);
     }
 
+// normScore lives in a column of the SQL chain table and nowhere in a bigChain file, so
+// only the SQL side can color by it, which is also the only side the native loaders do.
+boolean useNormScore = !tg->isBigBed && (chainCart->chainColor == chainColorScoreColors) &&
+    chainDbNormScoreAvailable(tg->tdb);
+
 struct hash *chainHash = newHash(8);
 struct hash *mapPsls = NULL;
 struct quickLiftRange *range, *rangeList = quickLiftSourceRanges(quickLiftFile, chromName,
     winStart, winEnd, chainHash);
 struct linkedFeatures *list = NULL;
 
 for (range = rangeList; range != NULL; range = range->next)
     {
     struct chain *chain, *chainList;
+    struct hash *normScores = NULL;
     if (tg->isBigBed)
         chainList = chainLoadIdRangeHub(NULL, chainFile, linkFile, range->chrom,
             range->start, range->end, -1);
     else
+        {
         chainList = chainLoadRange(liftDb, table, range->chrom, range->start, range->end);
+        if (useNormScore)
+            normScores = chainNormScoreHash(liftDb, table, range);
+        }
 
     for (chain = chainList; chain != NULL; chain = chain->next)
         {
         if (chain->blockList == NULL)
             continue;
         if ((optionChrStr != NULL) && !startsWith(optionChrStr, chain->qName))
             continue;
         if ((chainCart->scoreFilter > 0) && (chain->score < chainCart->scoreFilter))
             continue;
 
         int qs = (chain->qStrand == '-') ? chain->qSize - chain->qEnd : chain->qStart;
         struct chain *lifted = quickLiftChain(chainHash, &mapPsls, chain);
         if (lifted == NULL)
             continue;
 
-        slAddHead(&list, lfFromLiftedChain(chain, lifted, qs, doSnake));
+        double normScore = -1;
+        if (normScores != NULL)
+            {
+            char id[32];
+            safef(id, sizeof id, "%d", chain->id);
+            char *val = hashFindVal(normScores, id);
+            if (val != NULL)
+                normScore = sqlFloat(val);
+            }
+        slAddHead(&list, lfFromLiftedChain(chain, lifted, qs, doSnake, normScore));
         }
+    hashFree(&normScores);
     }
 
 // put the list back into the order the chains were loaded in, which is what decides
 // between items the sort below sees as equal
 slReverse(&list);
 if (tg->visibility != tvDense)
     slSort(&list, linkedFeaturesCmpStart);
 else if (chainCart->chainColor == chainColorScoreColors)
     slSort(&list, chainCmpScore);
 tg->items = list;
 
 maybeLoadSnake(tg);
 }
 
 void bigChainLoadItems(struct track *tg)