63e3449141555055de52e908c50fed88cb5782ae
jcasper
  Wed May 27 07:43:25 2026 -0700
Resolving request to not impose title case on facet titles, refs #36320

diff --git src/hg/js/facetedComposite.js src/hg/js/facetedComposite.js
index 3b3c88af412..fa46e91eac9 100644
--- src/hg/js/facetedComposite.js
+++ src/hg/js/facetedComposite.js
@@ -21,34 +21,32 @@
         }
         const fetchUrl = `/cgi-bin/hgTrackUi?${fetchBody}`;
         const req = (fetchUrl.length > 2048 || embeddedData.udcTimeout) ?
             fetch("/cgi-bin/hgTrackUi", {
                 method: "POST",
                 headers: { "Content-Type": "application/x-www-form-urlencoded" },
                 body: fetchBody,
             })
             : fetch(fetchUrl, {
                 method: "GET",
                 headers: { "Content-Type": "application/x-www-form-urlencoded" },
             });
         return req.then(r => r.ok ? r.json() : null).catch(() => null);
     };
 
-    const toTitleCase = str =>
-          str.toLowerCase()
-          .split(/[_\s-]+/)  // Split on underscore, space, or hyphen
-          .map(word => word.charAt(0).toUpperCase() + word.slice(1)).join(" ");
+    const toTitleStyle = str =>
+            str.replace(/_+/g, " ");
 
     const escapeRegex = str => str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
 
     // For primaryKey values that use the 'id|label' form, return just the id.
     // The label is for display only; the cart and rowToIdx need the bare id.
     const primaryKeyId = v => {
         if (v == null) return v;
         const s = String(v);
         const bar = s.indexOf("|");
         return bar >= 0 ? s.slice(0, bar) : s;
     };
 
     const embeddedData = (() => {
         // get data that was embedded in the HTML here to use them as globals
         const dataTag = document.getElementById("app-data");
@@ -138,31 +136,31 @@
         const { metadata, rowToIdx, colNames } = allData;
 
         // Match subtrackUrls trackDb keys against metadata column names
         // ignoring leading underscores on either side, so authors can toggle
         // facet visibility by adding/removing a '_' prefix in the metadata
         // file without having to re-edit trackDb.
         const stripUnderscores = s => s.replace(/^_+/, "");
         const subtrackUrls = Object.fromEntries(
             Object.entries(embeddedData.subtrackUrls || {})
                   .map(([k, v]) => [stripUnderscores(k), v])
         );
 
         const ordinaryColumns = colNames.map(key => {
             const col = {
                 data: key,
-                title: toTitleCase(key.replace(/^_/, "")),
+                title: toTitleStyle(key.replace(/^_+/, "")),
             };
             const urlTemplate = subtrackUrls[stripUnderscores(key)];
             if (urlTemplate) {
                 // Mirrors hgc/hgc.c:printIdOrLinks(): split cell on ',', each
                 // token may be 'id|label' (id substitutes $$, label is shown).
                 // urlTemplate is html-encoded server-side (htmlEncode in
                 // hgTrackUi.c), so it's safe to interpolate into an href.
                 col.render = (data, type) => {
                     if (type !== "display") return data;
                     if (data == null || data === "") return "";
                     const parts = String(data).split(",")
                                               .map(s => s.trim())
                                               .filter(Boolean);
                     return parts.map(tok => {
                         let idForUrl = tok, label = tok, encode = true;
@@ -506,31 +504,31 @@
             });
             if (otherValue !== null) {
                 topToShow.push([otherKey, otherValue]);
             }
             if (topToShow.length <= 1) {  // no point if there's only one group
                 excludeCheckboxes.push(key);
                 return;
             }
 
             // --- Build the facet group with collapsible structure ---
             const facetDiv = document.createElement("div");
             facetDiv.classList.add("facet-group");
 
             // Clickable heading that toggles collapse
             const heading = Object.assign(document.createElement("strong"), {
-                textContent: toTitleCase(key),
+                textContent: toTitleStyle(key),
                 className: "facet-heading",
             });
             facetDiv.appendChild(heading);
 
             // Collapsible body: holds Clear button + all checkboxes
             const facetBody = document.createElement("div");
             facetBody.classList.add("facet-body");
 
             // Clear button — built here so it lives inside the collapsible body
             const clearBtn = document.createElement("button");
             clearBtn.textContent = "Clear";
             clearBtn.type = "button";
             facetBody.appendChild(clearBtn);
 
             // Build checkbox labels