Repository: plotly/plotly.py

Issue:
histogram y-axis label should take into account histnorm and barnorm
dupe of plotly/plotly_express#65 ?


Discussion:
This issue came up again in the community forum https://community.plot.ly/t/bug-report-on-px-histograms-histnorm-parameter/29092
See also https://github.com/plotly/plotly.py/issues/2876
Something like:

* `<histnorm>` when `histfunc` is null: "probability" (rather than "count (probability)")
* `<histnorm> of <histfunc> of <y> (<histnorm>)`: "density of sum of cost" 
* `<histfunc> of <y> (normalized as <barnorm>)`: "count (normalized as percentage)" or "sum of cost (normalized as fraction)", and for non-hist bars, "cost (normalized as fraction)"


Resolving patch:
diff --git a/packages/python/plotly/plotly/express/_core.py b/packages/python/plotly/plotly/express/_core.py
--- a/packages/python/plotly/plotly/express/_core.py
+++ b/packages/python/plotly/plotly/express/_core.py
@@ -149,16 +149,22 @@ def _is_continuous(df, col_name):
 def get_decorated_label(args, column, role):
     label = get_label(args, column)
     if "histfunc" in args and (
-        (role == "x" and "orientation" in args and args["orientation"] == "h")
+        (role == "z")
+        or (role == "x" and "orientation" in args and args["orientation"] == "h")
         or (role == "y" and "orientation" in args and args["orientation"] == "v")
-        or (role == "z")
     ):
         if label:
-            return "%s of %s" % (args["histfunc"] or "count", label)
+            label = "%s of %s" % (args["histfunc"] or "count", label)
         else:
-            return "count"
-    else:
-        return label
+            label = "count"
+
+        if "histnorm" in args and args["histnorm"] is not None:
+            label = "%s of %s" % (args["histnorm"], label)
+
+        if "barnorm" in args and args["barnorm"] is not None:
+            label = "%s (normalized as %s)" % (label, args["barnorm"])
+
+    return label
 
 
 def make_mapping(args, variable):
@@ -264,14 +270,7 @@ def make_trace_kwargs(args, trace_spec, trace_data, mapping_labels, sizeref):
                 mapping_labels["%{xaxis.title.text}"] = "%{x}"
                 mapping_labels["%{yaxis.title.text}"] = "%{y}"
 
-        elif (
-            attr_value is not None
-            or (trace_spec.constructor == go.Histogram and attr_name in ["x", "y"])
-            or (
-                trace_spec.constructor in [go.Histogram2d, go.Histogram2dContour]
-                and attr_name == "z"
-            )
-        ):
+        elif attr_value is not None:
             if attr_name == "size":
                 if "marker" not in trace_patch:
                     trace_patch["marker"] = dict()
@@ -464,13 +463,15 @@ def make_trace_kwargs(args, trace_spec, trace_data, mapping_labels, sizeref):
                 else:
                     trace_patch[attr_name] = trace_data[attr_value]
             else:
-                if attr_value:
-                    trace_patch[attr_name] = trace_data[attr_value]
+                trace_patch[attr_name] = trace_data[attr_value]
                 mapping_labels[attr_label] = "%%{%s}" % attr_name
-    if trace_spec.constructor not in [
-        go.Parcoords,
-        go.Parcats,
-    ]:
+        elif (trace_spec.constructor == go.Histogram and attr_name in ["x", "y"]) or (
+            trace_spec.constructor in [go.Histogram2d, go.Histogram2dContour]
+            and attr_name == "z"
+        ):
+            # ensure that stuff like "count" gets into the hoverlabel
+            mapping_labels[attr_label] = "%%{%s}" % attr_name
+    if trace_spec.constructor not in [go.Parcoords, go.Parcats]:
         # Modify mapping_labels according to hover_data keys
         # if hover_data is a dict
         mapping_labels_copy = OrderedDict(mapping_labels)


Test patch:
diff --git a/packages/python/plotly/plotly/tests/test_core/test_px/test_px_functions.py b/packages/python/plotly/plotly/tests/test_core/test_px/test_px_functions.py
--- a/packages/python/plotly/plotly/tests/test_core/test_px/test_px_functions.py
+++ b/packages/python/plotly/plotly/tests/test_core/test_px/test_px_functions.py
@@ -267,7 +267,7 @@ def test_sunburst_treemap_column_parent():
     df = pd.DataFrame(dict(id=vendors, sectors=sectors, parent=regions, values=values,))
     path = ["parent", "sectors", "id"]
     # One column of the path is a reserved name - this is ok and should not raise
-    fig = px.sunburst(df, path=path, values="values")
+    px.sunburst(df, path=path, values="values")
 
 
 def test_sunburst_treemap_with_path_non_rectangular():
@@ -379,6 +379,31 @@ def test_parcats_dimensions_max():
     assert [d.label for d in fig.data[0].dimensions] == ["sex", "smoker", "day", "size"]
 
 
+def test_histfunc_hoverlabels():
+    df = px.data.tips()
+    fig = px.histogram(df, x="total_bill")
+    label = "count"
+    assert fig.layout.yaxis.title.text == label
+    assert label + "=" in fig.data[0].hovertemplate
+
+    fig = px.histogram(df, x="total_bill", y="tip")
+    label = "sum of tip"
+    assert fig.layout.yaxis.title.text == label
+    assert label + "=" in fig.data[0].hovertemplate
+
+    fig = px.histogram(
+        df,
+        x="total_bill",
+        y="tip",
+        histfunc="min",
+        histnorm="probability",
+        barnorm="percent",
+    )
+    label = "probability of min of tip (normalized as percent)"
+    assert fig.layout.yaxis.title.text == label
+    assert label + "=" in fig.data[0].hovertemplate
+
+
 def test_timeline():
     df = pd.DataFrame(
         [