5bfb32fdfc639f9bffb5435044cef164bdde0b28
kent
  Sun Dec 12 08:02:13 2021 -0800
Implementing barChartMerge trackDb option for faceted bar charts.

diff --git src/hg/lib/tablesTables.c src/hg/lib/tablesTables.c
index dff300a..7612139 100644
--- src/hg/lib/tablesTables.c
+++ src/hg/lib/tablesTables.c
@@ -155,31 +155,33 @@
 static void showTableFilterControlRow(struct fieldedTable *table, struct slName *visibleFields,
     struct cart *cart, char *varPrefix, int maxLenField, struct hash *suggestHash)
 /* Assuming we are in table already drow control row.
  * The suggestHash is keyed by field name.  If something is there we'll assume
  * it's value is slName list of suggestion values */
 {
 /* Include javascript and style we need  */
 printf("<link rel='stylesheet' href='//code.jquery.com/ui/1.12.1/themes/base/jquery-ui.css'>\n");
 printf("<script src='https://code.jquery.com/ui/1.12.1/jquery-ui.js'></script>\n");
 
 printf("<tr>");
 struct slName *el;
 for (el = visibleFields; el != NULL; el = el->next)
     {
     char *field = el->name;
-    int fieldIx = fieldedTableMustFindFieldIx(table, field);
+    int fieldIx = fieldedTableFindFieldIx(table, field);
+    if (fieldIx >= 0)
+	{
 	char varName[256];
 	safef(varName, sizeof(varName), "%s_f_%s", varPrefix, field);
 	printf("<td>");
 
 	/* Approximate size of input control in characters */
 	int size = fieldedTableMaxColChars(table, fieldIx);
 	if (size > maxLenField)
 	    size = maxLenField;
 
 	/* Print input control getting previous value from cart.  Set an id=
 	 * so auto-suggest can find this control. */
 	char *oldVal = cartUsualString(cart, varName, "");
 	printf("<INPUT TYPE=TEXT NAME=\"%s\" id=\"%s\" SIZE=%d",
 	    varName, varName, size+1);
 	if (isEmpty(oldVal))
@@ -189,158 +191,172 @@
 
 	/* Write out javascript to reset page number to 1 if filter changes */
 	resetPageNumberOnChange(varName, varPrefix);
 
 	/* Set up the auto-suggest list for this filter */
 	if (suggestHash != NULL)
 	    {
 	    struct slName *suggestList = hashFindVal(suggestHash, field);
 	    if (suggestList != NULL)
 		{
 		printSuggestScript(varName, suggestList);
 		}
 	    }
 	printf("</td>\n");
 	}
+    }
 
 
 printf("</TR>");
 }
 
 static void showTableSortingLabelRow(struct fieldedTable *table, struct slName *visibleFields, 
     struct cart *cart, char *varPrefix, char *returnUrl)
 /* Put up the label row with sorting fields attached.  ALso actually sort table.  */
 {
 /* Get order var */
 char orderVar[256];
 safef(orderVar, sizeof(orderVar), "%s_order", varPrefix);
 char *orderFields = cartUsualString(cart, orderVar, "");
 
 char pageVar[64];
 safef(pageVar, sizeof(pageVar), "%s_page", varPrefix);
 
 /* Print column labels */
 struct slName *vis;
 for (vis = visibleFields; vis != NULL; vis = vis->next)
     {
+    if (fieldedTableFindFieldIx(table, vis->name) != -1)
+	{
 	printf("<td>");
 	printf("<A class=\"topbar\" HREF=\"");
 	printf("%s", returnUrl);
 	printf("&%s=1", pageVar);
 	printf("&%s=", orderVar);
 	char *field = vis->name;
 	if (!isEmpty(orderFields) && sameString(orderFields, field))
 	    printf("-");
 	printf("%s", field);
 	printf("\">");
 	printf("%s", field);
 	if (!isEmpty(orderFields))
 	    {
 	    char *s = orderFields;
 	    boolean isRev = (s[0] == '-');
 	    if (isRev)
 		++s;
 	    if (sameString(field, s))
 		{
 		if (isRev)
 		    printf("&uarr;");
 		else
 		    printf("&darr;");
 		}
 	    }
 	printf("</A>");
 	printf("</td>\n");
 	}
+    }
 
 /* Sort on field */
 if (!isEmpty(orderFields))
     {
     boolean doReverse = FALSE;
     char *field = orderFields;
     if (field[0] == '-')
         {
 	field += 1;
 	doReverse = TRUE;
 	}
     fieldedTableSortOnField(table, field, doReverse);
     }
 }
 
 static void showTableDataRows(struct fieldedTable *table, struct slName *visibleFields,
     int pageSize, int maxLenField,
     struct hash *tagOutputWrappers, void *wrapperContext)
 /* Render data rows into HTML */
 {
 /* Look up visible fields in table */
 int visFieldCount = slCount(visibleFields);
 int visIx[visFieldCount];
 int i;
 struct slName *el = visibleFields;;
 for (i=0; i<visFieldCount; ++i, el = el->next)
-    visIx[i] = fieldedTableMustFindFieldIx(table, el->name);
+    visIx[i] = fieldedTableFindFieldIx(table, el->name);
 
 /* Figure out numerical ones */
 int count = 0;
 struct fieldedRow *row;
 boolean isNum[visFieldCount];
 for (i=0; i<visFieldCount; ++i)
+    {
+    int vix = visIx[i];
+    if (vix >= 0)
 	isNum[i] = fieldedTableColumnIsNumeric(table, visIx[i]);
+    else
+        isNum[i] = FALSE;
+    }
 
 for (row = table->rowList; row != NULL; row = row->next)
     {
     if (++count > pageSize)
          break;
     printf("<TR>\n");
     int fieldIx = 0;
     int i;
     for (i=0; i<visFieldCount; ++i)
 	{
 	fieldIx = visIx[i];
+	if (fieldIx >= 0)
+	    {
 	    char shortVal[maxLenField+1];
 	    char *longVal = emptyForNull(row->row[fieldIx]);
 	    char *val = longVal;
 	    int valLen = strlen(val);
 	    if (maxLenField > 0 && maxLenField < valLen)
 		{
 		if (valLen > maxLenField)
 		    {
 		    memcpy(shortVal, val, maxLenField-3);
 		    shortVal[maxLenField-3] = 0;
 		    strcat(shortVal, "...");
 		    val = shortVal;
 		    }
 		}
-	if (isNum[fieldIx]) // vacuous, but left it just in case we want to do different stuff to numbers later
+	    if (isNum[fieldIx]) // vacuous, but left it just in case we want 
+				// to do different stuff to numbers later
 		printf("<td>");
 	    else
 		printf("<td>");
 	    boolean printed = FALSE;
 	    if (tagOutputWrappers != NULL && !isEmpty(val))
 		{
 		char *field = table->fields[fieldIx];
 		webTableOutputWrapperType *printer = hashFindVal(tagOutputWrappers, field);
 		if (printer != NULL)
 		    {
 		    printer(table, row, field, longVal, val, wrapperContext);
 		    printed = TRUE;
 		    }
 		
 		}
 	    if (!printed)
 		printf("%s", val);
 	    printf("</td>\n");
 	    }
+	}
     printf("</TR>\n");
     }
 }
 
 static void showTablePaging(struct fieldedTable *table, struct cart *cart, char *varPrefix,
     struct fieldedTableSegment *largerContext, int pageSize)
 /* If larger context exists and is bigger than current display, then draw paging controls. */
 {
 /* Handle paging if any */
 if (largerContext != NULL)  // Need to page?
      {
      if (pageSize < largerContext->tableSize)
 	{
 	int curPage = largerContext->tableOffset/pageSize;
 	int totalPages = (largerContext->tableSize + pageSize - 1)/pageSize;
@@ -397,107 +413,111 @@
 		"event.target.closest('form').submit();\n"
 		, varPrefix, totalPages);
 
 	    }
 	}
      }
 }
 
 void webFilteredFieldedTable(struct cart *cart, struct fieldedTable *table, 
     char *visibleFieldList, char *returnUrl, char *varPrefix,
     int maxLenField, struct hash *tagOutputWrappers, void *wrapperContext,
     boolean withFilters, char *pluralInstructions, 
     int pageSize, int facetUsualSize,
     struct fieldedTableSegment *largerContext, struct hash *suggestHash, 
     struct facetField **ffArray, char *visibleFacetList,
-    void (*addFunc)(int) )
+    void (*addFunc)(int), boolean facetMergeOk )
 /* Show a fielded table that can be sorted by clicking on column labels and optionally
  * that includes a row of filter controls above the labels .
  * The maxLenField is maximum character length of field before truncation with ...
  * Pass in 0 for no max. */
 {
 if (strchr(returnUrl, '?') == NULL)
      errAbort("Expecting returnUrl to include ? in showFieldedTable\nIt's %s", returnUrl);
 
 if (pluralInstructions != NULL)
     showTableFilterInstructionsEtc(table, pluralInstructions, largerContext, addFunc, 
 	    visibleFacetList, varPrefix);
 
 if (visibleFacetList)
     {
     // Show top bar with quick-deselects for selected facet values
     //  as well a clear restriction button that cleans out _filter cart var. 
 
     struct dyString *facetBar = dyStringNew(1024);
     char filterVar[256];
     safef(filterVar, sizeof(filterVar), "%s_filter", varPrefix);
 
     char *where = cartUsualString(cart, filterVar, "");
 
 
     boolean gotSelected = FALSE;
 
     struct slName *visList = slNameListFromComma(visibleFacetList);
     struct slName *vis;
     for (vis = visList; vis != NULL; vis = vis->next)
 	{
-	int f = fieldedTableMustFindFieldIx(table, vis->name);
-	struct facetField *field = ffArray[f];
+	int fIx = fieldedTableFindFieldIx(table, vis->name);
+	if (fIx >= 0)
+	    {
+	    struct facetField *field = ffArray[fIx];
 	    if (!field->allSelected)
 		{
 		gotSelected = TRUE;
 		htmlDyStringPrintf(facetBar, "<span class='card facet-card' style='display: inline-block;'><span class='card-body'>\n");
 		htmlDyStringPrintf(facetBar, "<dt style='display: inline-block;'>\n");
-	    htmlDyStringPrintf(facetBar, "<h6 class='card-title'>%s</h6></dt>\n", field->fieldName);
+		htmlDyStringPrintf(facetBar, "<h6 class='card-title'>%s</h6></dt>\n", 
+		    field->fieldName);
 
 		struct facetVal *val;
 
 		// Sort values alphabetically
 		// Make a copy to not disturb the original order 
 		struct facetVal *valListCopy = facetsClone(field->valList);
 		slSort(&valListCopy, facetValCmp);
 		
 		for (val = valListCopy; val; val=val->next)
 		    {
 		    boolean specificallySelected = (val->selected && !field->allSelected);
 		    if (specificallySelected)
 			{
 			char *op = "remove";
-		    htmlDyStringPrintf(facetBar, "<dd class=\"facet\" style='display: inline-block;'>\n");
-		    htmlDyStringPrintf(facetBar, "<input type=checkbox value=%s class=ttFsCheckBox %s>&nbsp;",
+			htmlDyStringPrintf(facetBar, 
+			    "<dd class=\"facet\" style='display: inline-block;'>\n");
+			htmlDyStringPrintf(facetBar, 
+			    "<input type=checkbox value=%s class=ttFsCheckBox %s>&nbsp;",
 			    specificallySelected ? "true" : "false", 
 			    specificallySelected ? "checked" : "");
 			htmlDyStringPrintf(facetBar, "<a href='%s"
 				"&%s_facet_op=%s|url|"
 				"&%s_facet_fieldName=%s|url|"
 				"&%s_facet_fieldVal=%s|url|"
 				"&%s_page=1'"
 				">",
 			    returnUrl, varPrefix,
 			    op, varPrefix, field->fieldName, varPrefix, val->val, varPrefix
 			    );
 			htmlDyStringPrintf(facetBar, "%s (%d)</a>", 
 			    naForEmpty(val->val), val->selectCount);
 			htmlDyStringPrintf(facetBar, "</dd>\n");
 			}
 		    }
 		slFreeList(&valListCopy);
 		
 		htmlDyStringPrintf(facetBar, "</span></span>\n");
-
 		}
-
+	    }
 	}
 
     if (!isEmpty(where) || gotSelected)
         {
 	printf("<div>\n");
         }
 
     if (!isEmpty(where))
 	{
 	// left column
         
 	printf("Restricting files to where %s. ", where);
 
 	printf("&nbsp&nbsp;");
 	printf("<input class='btn btn-secondary' type='button' id='clearRestrictionButton' VALUE=\"Clear Restriction\">");
@@ -535,67 +555,106 @@
 
     dyStringFree(&facetBar);
     }
 
 printf("<div class='row'>\n"); // parent container
 
 if (visibleFacetList)
     {
     // left column
     printf("<div class='col-xs-6 col-sm-4 col-md-4 col-lg-3 col-xl-3'>\n");
 
     struct slName *visList = slNameListFromComma(visibleFacetList);
     struct slName *vis;
     for (vis = visList; vis != NULL; vis = vis->next)
 	{
+	char *fieldName = vis->name;
 	char selfId[256];
-	safef(selfId, sizeof(selfId), "%s_self_a_%s", varPrefix, vis->name);
+	safef(selfId, sizeof(selfId), "%s_self_a_%s", varPrefix, fieldName);
 	subChar(selfId, ' ', '_');
 
-	int f = fieldedTableMustFindFieldIx(table, vis->name);
-	struct facetField *field = ffArray[f];
+	/* Work on facet field label line */
 	htmlPrintf("<div id=\"%s\" class='card facet-card'><div class='card-body'>\n", selfId);
-	htmlPrintf("<h6 class='card-title'>%s</h6><dl>\n", field->fieldName);
-	struct facetVal *val;
+	htmlPrintf("<h6 class='card-title'>%s",vis->name);
+
+	int f = fieldedTableFindFieldIx(table, fieldName);
+	char *op = "unmerge";
+	struct facetField *field = NULL;
+	if (f >= 0)
+	    {
+	    field = ffArray[f];
+	    if (!field->isMerged)
+	        op = "merge";
+	    }
+
+	/* Write merge/unmerge link and number of categories */
+	if (facetMergeOk)
+	    {
+	    htmlPrintf("<span style='float:right'>");
+	    htmlPrintf("<a class='btn btn-secondary' href='%s"
+		    "&%s_facet_op=%s|none|"
+		    "&%s_facet_fieldName=%s|url|"
+		    "&%s_facet_fieldVal=%s|url|"
+		    "&%s_page=1' "
+		    ">", 
+		    returnUrl, varPrefix, op, varPrefix, fieldName, 
+		    varPrefix, "", varPrefix);
+	    htmlPrintf("%s", op);
 
+	    if (field != NULL && sameString(op, "merge"))
+		{
+		int selectedFieldCount = facetFieldCountSelected(field);
+		htmlPrintf(" %d", selectedFieldCount);
+		}
+	    htmlPrintf("</a></span>");
+	    }
+
+	/* CLose up facet field label line */
+	htmlPrintf("</h6><dl>\n");
+
+	if (field != NULL)
+	    {
+	    struct facetVal *val;
 	    if (!field->allSelected)  // add reset facet link
 		{
 		char *op = "reset";
 		htmlPrintf("<dd><a class='btn btn-secondary' href='%s"
 			"&%s_facet_op=%s|url|"
 			"&%s_facet_fieldName=%s|url|"
 			"&%s_facet_fieldVal=%s|url|"
 			"&%s_page=1' "
 			">%s</a></dd>\n",
 		    returnUrl, varPrefix, op, 
 		    varPrefix, field->fieldName, varPrefix, "", varPrefix,
 		    "Clear"
 		    );
 		}
 
 	    int valuesShown = 0;
 	    int valuesNotShown = 0;
 	    if (field->showAllValues)  // Sort alphabetically if they want all values 
 		{
 		slSort(&field->valList, facetValCmp);
 		}
 	    int extraAnchorPeriod = 15;
 	    int extraAnchorPos = 0;
 	    for (val = field->valList; val; val=val->next)
 		{
 		boolean specificallySelected = (val->selected && !field->allSelected);
-	    if ((val->selectCount > 0 && (field->showAllValues || valuesShown < facetUsualSize))
+		if ((val->selectCount > 0 && 
+		    (field->showAllValues || valuesShown < facetUsualSize) && 
+		    !field->isMerged)
 		    || specificallySelected)
 		    {
 		    ++valuesShown;
 		    ++extraAnchorPos;
 		    char *op = "add";
 		    if (specificallySelected)
 			op = "remove";
 		    printf("<dd class=\"facet\"");
 		    if (extraAnchorPos >= extraAnchorPeriod)
 			{
 			safef(selfId, sizeof(selfId), "%s_self_a_%s_%s", varPrefix, vis->name, 
 			    val->val);
 			subChar(selfId, ' ', '_');
 			printf(" id=\"%s\"", selfId);
 			extraAnchorPos= 0;
@@ -611,61 +670,62 @@
 			    "&%s_page=1#%s' "
 			    ">",
 			returnUrl, varPrefix,
 			op, varPrefix, field->fieldName, varPrefix, val->val, varPrefix, selfId
 			);
 		    htmlPrintf("%s (%d)</a>", naForEmpty(val->val), val->selectCount);
 		    printf("</dd>\n");
 		    }
 		else if (val->selectCount > 0)
 		    {
 		    ++valuesNotShown;
 		    }
 		}
 
 	    // show "See More" link when facet has lots of values
-	if (valuesNotShown > 0)
+	    if (valuesNotShown > 0 && !field->isMerged)
 		{
 		char *op = "showAllValues";
 		htmlPrintf("<dd><a href='%s"
 			"&%s_facet_op=%s|url|"
 			"&%s_facet_fieldName=%s|url|"
 			"&%s_facet_fieldVal=%s|url|"
 			"&%s_page=1#%s' "
 			">See %d More</a></dd>\n",
 		    returnUrl, varPrefix, op, 
 		    varPrefix, field->fieldName, varPrefix, "", 
 		    varPrefix, selfId, valuesNotShown
 		    );
 		}
 
 	    // show "See Fewer" link when facet has lots of values
 	    if (field->showAllValues && valuesShown >= facetUsualSize)
 		{
 		safef(selfId, sizeof(selfId), "%s_self_a_%s", varPrefix, vis->name);
 		subChar(selfId, ' ', '_');
 		char *op = "showSomeValues";
 		htmlPrintf("<dd><a href='%s"
 			"&%s_facet_op=%s|url|"
 			"&%s_facet_fieldName=%s|url|"
 			"&%s_facet_fieldVal=%s|url|"
 			"&%s_page=1#%s' "
 			">%s</a></dd>\n",
 		    returnUrl, varPrefix, op, varPrefix, field->fieldName, varPrefix, "", varPrefix,
 		    selfId, "See Fewer"
 		    );
 		}
+	    }
 	htmlPrintf("</div></div>\n");
 	}
     printf("</div>\n");
     // Clicking a checkbox is actually a click on the following link
     jsInlineF(
 	"$(function () {\n"
 	"  $('.ttFsCheckBox').click(function() {\n"
 	"    this.nextSibling.nextSibling.click();\n"
 	"  });\n"
 	"});\n");
     }
 
 // start right column, if there are two columns
 if (visibleFacetList)
     printf("<div class='col-xs-6 col-sm-8 col-md-8 col-lg-9 col-xl-9'>\n");
@@ -700,31 +760,31 @@
 
 printf("</div>\n"); //close parent container
 }
 
 void webSortableFieldedTable(struct cart *cart, struct fieldedTable *table, 
     char *returnUrl, char *varPrefix,
     int maxLenField, struct hash *tagOutputWrappers, void *wrapperContext)
 /* Display all of table including a sortable label row.  The tagOutputWrappers
  * is an optional way to enrich output of specific columns of the table.  It is keyed
  * by column name and has for values functions of type webTableOutputWrapperType. */
 {
 webFilteredFieldedTable(cart, table, NULL, returnUrl, varPrefix, 
     maxLenField, tagOutputWrappers, wrapperContext,
     FALSE, NULL, 
     slCount(table->rowList), 
-    0, NULL, NULL, NULL, NULL, NULL);
+    0, NULL, NULL, NULL, NULL, NULL, FALSE);
 }
 
 
 void webTableBuildQuery(struct cart *cart, char *from, char *initialWhere, 
     char *varPrefix, char *fields, boolean withFilters, 
     struct dyString **retQuery, struct dyString **retWhere)
 /* Construct select, from and where clauses in query, keeping an additional copy of where 
  * Returns the SQL query and the SQL where expression as two dyStrings (need to be freed)  */
 {
 struct dyString *query = dyStringNew(0);
 struct dyString *where = dyStringNew(0);
 struct slName *field, *fieldList = commaSepToSlNames(fields);
 boolean gotWhere = FALSE;
 sqlDyStringPrintf(query, "select %-s from %-s", sqlCkIl(fields), sqlCkIl(from));
 if (!isEmpty(initialWhere))
@@ -911,23 +971,23 @@
     {
     int lastPage = (context.tableSize-1)/pageSize;
     if (page > lastPage)
         page = lastPage;
     context.tableOffset = page * pageSize;
     }
 
 if (!visibleFacetList)
     {
     sqlDyStringPrintf(query, " limit %d offset %d", pageSize, context.tableOffset);
     table = fieldedTableFromDbQuery(conn, query->string);
     }
 
 webFilteredFieldedTable(cart, table, fields, returnUrl, varPrefix, maxFieldWidth, 
     tagOutWrappers, wrapperContext, withFilters, pluralInstructions, 
-    pageSize, facetUsualSize, &context, suggestHash, ffArray, visibleFacetList, addFunc);
+    pageSize, facetUsualSize, &context, suggestHash, ffArray, visibleFacetList, addFunc, FALSE);
 fieldedTableFree(&table);
 
 dyStringFree(&fusedFields);
 dyStringFree(&query);
 dyStringFree(&where);
 }