ef4a5ad1ea45eda4fde693f56f2106c553e87d93
chmalee
  Wed May 20 15:24:30 2026 -0700
Add cnv type to myVariants, refs #33808

diff --git src/hg/hgTracks/myVariantsTrack.c src/hg/hgTracks/myVariantsTrack.c
index 51497d5b586..791e2b3d0f6 100644
--- src/hg/hgTracks/myVariantsTrack.c
+++ src/hg/hgTracks/myVariantsTrack.c
@@ -16,31 +16,31 @@
 #include "myVariantsShare.h"
 #include "jsonParse.h"
 #include "sqlNum.h"
 #include "customFactory.h"
 #include "hgConfig.h"
 #include "htmlColor.h"
 #include "wikiLink.h"
 #include "hgFind.h"
 #include "hgHgvs.h"
 #include "jsonWrite.h"
 
 static char *reservedFieldNames[] = {
     "bin", "chrom", "chromStart", "chromEnd", "name", "score", "strand",
     "thickStart", "thickEnd", "itemRgb", "blockCount", "blockSizes",
     "chromStarts", "description", "db", "ref", "alt", "project",
-    "mouseover", "id", NULL
+    "mouseover", "itemType", "cnvType", "id", NULL
 };
 
 static char *truncateSeq(char *seq, int maxLen)
 /* Return seq cloned and truncated to maxLen with "..." appended if longer.
  * NULL or empty input returns NULL. */
 {
 if (isEmpty(seq))
     return NULL;
 if (strlen(seq) <= (size_t)maxLen)
     return cloneString(seq);
 struct dyString *dy = dyStringNew(maxLen + 4);
 dyStringAppendN(dy, seq, maxLen);
 dyStringAppend(dy, "...");
 return dyStringCannibalize(&dy);
 }
@@ -334,109 +334,150 @@
                 item->score = 0;
                 item->bin = binFromRange(item->chromStart, item->chromEnd);
                 char *hgvsRef = NULL, *hgvsAlt = NULL, *changeSeq = NULL;
                 enum hgvsChangeType changeType = hgvsctUndefined;
                 extractHgvsChange(hgvs, &hgvsRef, &hgvsAlt, &changeType, &changeSeq);
                 item->ref = hgvsRef ? hgvsRef : cloneString("");
                 item->alt = hgvsAlt ? hgvsAlt : cloneString("");
                 item->name = synthesizeItemName(item->ref, item->alt, changeType, changeSeq);
                 if (item->name == NULL)
                     item->name = cloneString("");
                 freeMem(changeSeq);
                 item->db = database;
                 item->description = cloneString(hgp->singlePos->description);
                 item->project = cloneString("");
                 item->mouseover = cloneString("");
+                item->itemType = cloneString("snv");
+                item->cnvType = cloneString("");
                 }
             }
         else
             {
             /* HGVS didn't match - try hgFind for position/gene resolution */
             struct hgPositions *hgpFind = hgPositionsFind(database, hgvs,
                 "", "myVariants", cart, FALSE, FALSE, NULL);
             if (hgpFind != NULL && hgpFind->posCount == 1)
                 {
                 struct hgPos *pos = hgpFind->singlePos;
                 item->chrom = cloneString(pos->chrom);
                 item->chromStart = item->thickStart = pos->chromStart;
                 item->chromEnd = item->thickEnd = pos->chromEnd;
                 item->strand[0] = '.';
                 item->score = 0;
                 item->bin = binFromRange(item->chromStart, item->chromEnd);
                 item->ref = cloneString("");
                 item->alt = cloneString("");
                 item->name = cloneString("");
                 item->db = database;
                 item->description = cloneString(pos->description ? pos->description : "");
                 item->project = cloneString("");
                 item->mouseover = cloneString("");
+                item->itemType = cloneString("snv");
+                item->cnvType = cloneString("");
                 }
             else if (hgpFind != NULL && hgpFind->posCount > 1)
                 {
                 warn("Position '%s' matches %d locations - please be more specific", hgvs, hgpFind->posCount);
                 return;
                 }
             else
                 {
                 warn("Position '%s' not found", hgvs);
                 return;
                 }
             }
         }
     }
 else
     {
     char *chrom = jsonStringVal(jsonFindNamedField(json, "jsonData", "chrom"), "chrom");
     int chromStart = sqlUnsigned(jsonStringVal(jsonFindNamedField(json, "jsonData", "start"), "start"));
     int chromEnd = sqlUnsigned(jsonStringVal(jsonFindNamedField(json, "jsonData", "end"), "end"));
     char *name = jsonStringVal(jsonFindNamedField(json, "jsonData", "name"), "name");
     int score = sqlUnsigned(jsonStringVal(jsonFindNamedField(json, "jsonData", "score"), "score"));
     char *strand = jsonStringVal(jsonFindNamedField(json, "jsonData", "strand"), "strand");
     int thickStart = sqlUnsigned(jsonStringVal(jsonFindNamedField(json, "jsonData", "thickStart"), "thickStart"));
     int thickEnd = sqlUnsigned(jsonStringVal(jsonFindNamedField(json, "jsonData", "thickEnd"), "thickEnd"));
     char *colorCode = jsonStringVal(jsonFindNamedField(json, "jsonData", "color"), "color");
     char *description = jsonStringVal(jsonFindNamedField(json, "jsonData", "description"), "description");
     char *ref = jsonOptionalStringField(json, "ref", "");
     char *alt = jsonOptionalStringField(json, "alt", "");
     char *project = jsonOptionalStringField(json, "project", "");
     char *mouseover = jsonOptionalStringField(json, "mouseover", "");
+    char *itemType = myVariantsCanonicalItemType(
+        jsonOptionalStringField(json, "itemType", "snv"));
+    if (itemType == NULL)
+        errAbort("invalid itemType");
+    char *cnvType = "";
+    if (sameString(itemType, "cnv"))
+        {
+        cnvType = myVariantsCanonicalCnvType(jsonOptionalStringField(json, "cnvType", ""));
+        if (cnvType == NULL)
+            errAbort("invalid cnvType");
+        }
     unsigned color;
     htmlColorForCode(colorCode, &color);
 
     item->bin = binFromRange(chromStart, chromEnd);
     item->chrom = cloneString(chrom);
     item->chromStart = chromStart;
     item->chromEnd = chromEnd;
+    /* SNV/CNV items don't carry a thick range or BED12 blocks; force defaults
+     * so a stale value from a view toggle in the client can't leak through. */
+    if (sameString(itemType, "snv") || sameString(itemType, "cnv"))
+        {
+        item->thickStart = chromStart;
+        item->thickEnd = chromEnd;
+        }
+    else
+        {
         item->thickStart = thickStart;
         item->thickEnd = thickEnd;
+        }
     if (isEmpty(name))
         {
         item->name = synthesizeItemName(ref, alt, hgvsctUndefined, NULL);
         if (item->name == NULL)
             item->name = cloneString("");
         }
     else
         item->name = cloneString(name);
     item->score = score;
     item->strand[0] = strand[0];
     item->itemRgb = color;
     item->description = cloneString(description);
+    if (sameString(itemType, "transcript"))
+        {
+        item->ref = cloneString("");
+        item->alt = cloneString("");
+        }
+    else if (sameString(itemType, "cnv"))
+        {
+        /* CNV stores the inserted/duplicated sequence in alt; ref is unused. */
+        item->ref = cloneString("");
+        item->alt = cloneString(alt);
+        }
+    else
+        {
         item->ref = cloneString(ref);
         item->alt = cloneString(alt);
+        }
     item->db = database;
     item->project = cloneString(project);
     item->mouseover = cloneString(mouseover);
+    item->itemType = cloneString(itemType);
+    item->cnvType = cloneString(cnvType);
     }
 
 if (!item)
     return;
 
 /* Parse blocks from JSON if present. Empty / missing means single full-span
  * block; ensureItemBlocks synthesizes that below. */
 struct jsonElement *blockSizesJson = jsonFindNamedField(json, "jsonData", "blockSizes");
 struct jsonElement *chromStartsJson = jsonFindNamedField(json, "jsonData", "chromStarts");
 if (blockSizesJson && blockSizesJson->type == jsonList &&
     chromStartsJson && chromStartsJson->type == jsonList)
     {
     int sizeN = slCount(blockSizesJson->val.jeList);
     int startN = slCount(chromStartsJson->val.jeList);
     if (sizeN != startN)
@@ -556,30 +597,36 @@
     unsigned color;
     if (htmlColorForCode(newVal, &color))
         {
         struct dyString *sql = sqlDyStringCreate(
             "update %s set %s='%d' where id=%d", tableName, fieldName, color, id);
         if (isNotEmpty(scopeDb))
             sqlDyStringPrintf(sql, " and db='%s'", scopeDb);
         if (isNotEmpty(scopeProject) && !sameString(scopeProject, "*"))
             sqlDyStringPrintf(sql, " and project='%s'", scopeProject);
         sqlUpdate(conn, sql->string);
         dyStringFree(&sql);
         cartRemove(cart, varName);
         }
     return;
     }
+if (sameString(fieldName, "cnvType"))
+    {
+    if (isNotEmpty(newVal) && myVariantsCanonicalCnvType(newVal) == NULL)
+        errAbort("invalid cnvType");
+    newVal = isEmpty(newVal) ? "" : myVariantsCanonicalCnvType(newVal);
+    }
 struct dyString *sql = sqlDyStringCreate("update %s set %s='%s' where id=%d",
     tableName, fieldName, newVal, id);
 if (isNotEmpty(scopeDb))
     sqlDyStringPrintf(sql, " and db='%s'", scopeDb);
 if (isNotEmpty(scopeProject) && !sameString(scopeProject, "*"))
     sqlDyStringPrintf(sql, " and project='%s'", scopeProject);
 sqlUpdate(conn, sql->string);
 dyStringFree(&sql);
 cartRemove(cart, varName);
 }
 
 static void updateBlocksFields(char *trackName, struct sqlConnection *conn,
         char *tableName, int id, char *scopeProject, char *scopeDb)
 /* Pull <trackName>_blockCount, _blockSizes, _chromStarts from the cart,
  * validate jointly against the row's current chromStart/chromEnd via
@@ -709,35 +756,38 @@
                 char ctVar[256];
                 safef(ctVar, sizeof ctVar, MYVARIANTS_FILE_VAR_PREFIX "%s", database);
                 cartSetString(cart, ctVar, ctFile);
                 freeMem(ctFile);
                 }
             }
         return;
         }
 
     /* Handle edits. Owner edits their own table, no project/db scope filter. */
     updateTextField(trackName, conn, tableName, "name", id, NULL, NULL);
     updateTextField(trackName, conn, tableName, "description", id, NULL, NULL);
     updateTextField(trackName, conn, tableName, "itemRgb", id, NULL, NULL);
     updateTextField(trackName, conn, tableName, "chromStart", id, NULL, NULL);
     updateTextField(trackName, conn, tableName, "chromEnd", id, NULL, NULL);
+    updateTextField(trackName, conn, tableName, "thickStart", id, NULL, NULL);
+    updateTextField(trackName, conn, tableName, "thickEnd", id, NULL, NULL);
     updateBlocksFields(trackName, conn, tableName, id, NULL, NULL);
     updateTextField(trackName, conn, tableName, "ref", id, NULL, NULL);
     updateTextField(trackName, conn, tableName, "alt", id, NULL, NULL);
     updateTextField(trackName, conn, tableName, "project", id, NULL, NULL);
     updateTextField(trackName, conn, tableName, "mouseover", id, NULL, NULL);
+    updateTextField(trackName, conn, tableName, "cnvType", id, NULL, NULL);
     /* Update any custom fields */
         {
         char *editUserName = getUserName();
         struct slName *customCols = myVariantsGetCustomFields(editUserName);
         struct slName *col;
         for (col = customCols; col != NULL; col = col->next)
             updateTextField(trackName, conn, tableName, col->name, id, NULL, NULL);
         slFreeList(&customCols);
         }
     /* Refresh the on-disk myVariants ctfile after edits. */
     {
         char *userName = getUserName();
         if (userName)
             {
             char *ctFile = myVariantsWriteCtFile(userName, database, cart);
@@ -1224,34 +1274,37 @@
         cartRemove(cart, varName);
         myVariantsShareFree(&liveShare);
         continue;
         }
 
     /* Apply field edits, scoped to the share's project/db so a recipient
      * cannot edit rows in projects/assemblies they were not granted. */
     char *sp = liveShare->project;
     char *sd = liveShare->db;
     struct sqlConnection *conn = hAllocConn(CUSTOM_TRASH);
     updateTextField(trackName, conn, tableName, "name", id, sp, sd);
     updateTextField(trackName, conn, tableName, "description", id, sp, sd);
     updateTextField(trackName, conn, tableName, "itemRgb", id, sp, sd);
     updateTextField(trackName, conn, tableName, "chromStart", id, sp, sd);
     updateTextField(trackName, conn, tableName, "chromEnd", id, sp, sd);
+    updateTextField(trackName, conn, tableName, "thickStart", id, sp, sd);
+    updateTextField(trackName, conn, tableName, "thickEnd", id, sp, sd);
     updateBlocksFields(trackName, conn, tableName, id, sp, sd);
     updateTextField(trackName, conn, tableName, "ref", id, sp, sd);
     updateTextField(trackName, conn, tableName, "alt", id, sp, sd);
     updateTextField(trackName, conn, tableName, "mouseover", id, sp, sd);
+    updateTextField(trackName, conn, tableName, "cnvType", id, sp, sd);
     struct slName *customCols = myVariantsGetCustomFields(liveShare->ownerUser);
     struct slName *col;
     for (col = customCols; col != NULL; col = col->next)
         updateTextField(trackName, conn, tableName, col->name, id, sp, sd);
     slFreeList(&customCols);
     hFreeConn(&conn);
     myVariantsShareFree(&liveShare);
     }
 hashElFreeList(&shareVars);
 }
 
 boolean myVariantsTrackEnabled()
 /* Return TRUE if the "My Variants" feature is enabled in hg.conf
  * and the current request comes from a valid user */
 {