Repository: streamlit/streamlit
Pull request: Add the label_visibility parameter to st.metric
## 📚 Context

Adds the `label_visibility` keyword-only parameter to `st.metric`, to be consistent with other elements that have the `label` parameter.

- What kind of change does this PR introduce?

  - [x] Bugfix
  - [x] Feature
  - [ ] Refactoring
  - [ ] Other, please describe:

## 🧠 Description of Changes

  - [ ] This is a breaking API change
  - [x] This is a visible (user-facing) change

**Visible:**
![image](https://user-images.githubusercontent.com/20672874/204869733-062efe35-71a0-49cd-88a6-e01848f37b97.png)

**Hidden:**
![image](https://user-images.githubusercontent.com/20672874/204869810-33746e2f-f154-4fd5-b56f-6cf3ed787417.png)

**Collapsed:**
![image](https://user-images.githubusercontent.com/20672874/204869926-a1a2d18b-959c-4c9c-b6a7-c0499943b231.png)

## 🧪 Testing Done

- [x] Screenshots included
- [x] Added/Updated unit tests
- [x] Added/Updated e2e tests

## 🌐 References

_Does this depend on other work, documents, or tickets?_

- **Issue**: Closes #5786 

---

**Contribution License Agreement**

By submitting this pull request you agree that all contributions to this project are made under the Apache 2.0 license.


Problem statement:
`st.metric` should have `label_visibility` parameter
We had that in the original spec but probably forgot it during implementation. Let's fix it. Not super urgent but would be good to do soon so we don't forget. 

---

Community voting on feature requests enables the Streamlit team to understand which features are most important to our users.

**If you'd like the Streamlit team to prioritize this feature request, please use the 👍 (thumbs up emoji) reaction in response to the initial post.**

Hints from issue discussion:

Commit message: Add unit tests for st.metric label_visibility

Patch to review:
diff --git a/frontend/src/components/elements/Metric/Metric.test.tsx b/frontend/src/components/elements/Metric/Metric.test.tsx
index 7c479bc2a4bf..8be6a20c4a5c 100644
--- a/frontend/src/components/elements/Metric/Metric.test.tsx
+++ b/frontend/src/components/elements/Metric/Metric.test.tsx
@@ -19,7 +19,10 @@ import { mount } from "src/lib/test_util"
 
 import StreamlitMarkdown from "src/components/shared/StreamlitMarkdown"
 
-import { Metric as MetricProto } from "src/autogen/proto"
+import {
+  Metric as MetricProto,
+  LabelVisibilityMessage as LabelVisibilityMessageProto,
+} from "src/autogen/proto"
 import Metric, { MetricProps } from "./Metric"
 
 const getProps = (elementProps: Partial<MetricProto> = {}): MetricProps => ({
@@ -47,6 +50,30 @@ describe("Metric element", () => {
     expect(wrappedMetricLabel.props().isLabel).toBe(true)
   })
 
+  it("pass labelVisibility prop to StyledTruncateText correctly when hidden", () => {
+    const props = getProps({
+      labelVisibility: {
+        value: LabelVisibilityMessageProto.LabelVisibilityOptions.HIDDEN,
+      },
+    })
+    const wrapper = mount(<Metric {...props} />)
+    expect(
+      wrapper.find("StyledTruncateText").first().prop("visibility")
+    ).toEqual(LabelVisibilityMessageProto.LabelVisibilityOptions.HIDDEN)
+  })
+
+  it("pass labelVisibility prop to StyledTruncateText correctly when collapsed", () => {
+    const props = getProps({
+      labelVisibility: {
+        value: LabelVisibilityMessageProto.LabelVisibilityOptions.COLLAPSED,
+      },
+    })
+    const wrapper = mount(<Metric {...props} />)
+    expect(
+      wrapper.find("StyledTruncateText").first().prop("visibility")
+    ).toEqual(LabelVisibilityMessageProto.LabelVisibilityOptions.COLLAPSED)
+  })
+
   it("renders direction icon based on props", () => {
     const props = getProps()
     const wrapper = mount(<Metric {...props} />)
diff --git a/frontend/src/components/elements/Metric/Metric.tsx b/frontend/src/components/elements/Metric/Metric.tsx
index d13877427620..f78fb46fce04 100644
--- a/frontend/src/components/elements/Metric/Metric.tsx
+++ b/frontend/src/components/elements/Metric/Metric.tsx
@@ -17,6 +17,7 @@
 import React, { ReactElement } from "react"
 import { Metric as MetricProto } from "src/autogen/proto"
 import { Theme } from "src/theme"
+import { labelVisibilityProtoValueToEnum } from "src/lib/utils"
 import Icon from "src/components/shared/Icon"
 import { useTheme } from "@emotion/react"
 import { ArrowDownward, ArrowUpward } from "@emotion-icons/material-outlined"
@@ -75,7 +76,11 @@ export default function Metric({ element }: MetricProps): ReactElement {
   return (
     <div data-testid="metric-container">
       <StyledMetricLabelText data-testid="stMetricLabel">
-        <StyledTruncateText>
+        <StyledTruncateText
+          visibility={labelVisibilityProtoValueToEnum(
+            element.labelVisibility?.value
+          )}
+        >
           <StreamlitMarkdown
             source={element.label}
             allowHTML={false}
diff --git a/frontend/src/components/elements/Metric/styled-components.ts b/frontend/src/components/elements/Metric/styled-components.ts
index 43d2f605e0fd..3f600b0ce00e 100644
--- a/frontend/src/components/elements/Metric/styled-components.ts
+++ b/frontend/src/components/elements/Metric/styled-components.ts
@@ -16,20 +16,29 @@
 
 import styled from "@emotion/styled"
 import { StyledWidgetLabel } from "src/components/widgets/BaseWidget/styled-components"
+import { LabelVisibilityOptions } from "src/lib/utils"
 
-export const StyledTruncateText = styled.div(({ theme }) => ({
-  overflowWrap: "normal",
-  textOverflow: "ellipsis",
-  width: "100%",
-  overflow: "hidden",
-  whiteSpace: "nowrap",
-  fontFamily: theme.genericFonts.bodyFont,
-  lineHeight: theme.lineHeights.normal,
-  verticalAlign: "middle",
-  display: "flex",
-  flexDirection: "row",
-  alignItems: "center",
-}))
+export interface StyledTruncateTextProps {
+  visibility?: LabelVisibilityOptions
+}
+
+export const StyledTruncateText = styled.div<StyledTruncateTextProps>(
+  ({ theme, visibility }) => ({
+    overflowWrap: "normal",
+    textOverflow: "ellipsis",
+    width: "100%",
+    overflow: "hidden",
+    whiteSpace: "nowrap",
+    fontFamily: theme.genericFonts.bodyFont,
+    lineHeight: theme.lineHeights.normal,
+    verticalAlign: "middle",
+    display: visibility === LabelVisibilityOptions.Collapsed ? "none" : "flex",
+    visibility:
+      visibility === LabelVisibilityOptions.Hidden ? "hidden" : "visible",
+    flexDirection: "row",
+    alignItems: "center",
+  })
+)
 
 export const StyledMetricLabelText = styled(StyledWidgetLabel)(() => ({
   marginBottom: 0,
diff --git a/lib/streamlit/elements/metric.py b/lib/streamlit/elements/metric.py
index 5f65b4e65b04..d86339e01cca 100644
--- a/lib/streamlit/elements/metric.py
+++ b/lib/streamlit/elements/metric.py
@@ -18,10 +18,12 @@
 
 from typing_extensions import Literal, TypeAlias
 
+from streamlit.elements.utils import get_label_visibility_proto_value
 from streamlit.errors import StreamlitAPIException
 from streamlit.proto.Metric_pb2 import Metric as MetricProto
 from streamlit.runtime.metrics_util import gather_metrics
 from streamlit.string_util import clean_text
+from streamlit.type_util import LabelVisibility, maybe_raise_label_warnings
 
 if TYPE_CHECKING:
     import numpy as np
@@ -49,6 +51,7 @@ def metric(
         delta: Delta = None,
         delta_color: DeltaColor = "normal",
         help: Optional[str] = None,
+        label_visibility: LabelVisibility = "visible",
     ) -> "DeltaGenerator":
         """Display a metric in big bold font, with an optional indicator of how the metric changed.
 
@@ -79,6 +82,11 @@ def metric(
              regardless of its value.
         help : str
             An optional tooltip that gets displayed next to the metric label.
+        label_visibility : "visible" or "hidden" or "collapsed"
+            The visibility of the label. If "hidden", the label doesn't show but there
+            is still empty space for it (equivalent to label="").
+            If "collapsed", both the label and the space are removed. Default is
+            "visible". This argument can only be supplied by keyword.
 
         Example
         -------
@@ -112,6 +120,8 @@ def metric(
             height: 320px
 
         """
+        maybe_raise_label_warnings(label, label_visibility)
+
         metric_proto = MetricProto()
         metric_proto.body = self.parse_value(value)
         metric_proto.label = self.parse_label(label)
@@ -124,6 +134,9 @@ def metric(
         )
         metric_proto.color = color_and_direction.color
         metric_proto.direction = color_and_direction.direction
+        metric_proto.label_visibility.value = get_label_visibility_proto_value(
+            label_visibility
+        )
 
         return self.dg._enqueue("metric", metric_proto)
 
diff --git a/lib/tests/streamlit/elements/metric_test.py b/lib/tests/streamlit/elements/metric_test.py
index f754a6919d55..fbeaa6d72d07 100644
--- a/lib/tests/streamlit/elements/metric_test.py
+++ b/lib/tests/streamlit/elements/metric_test.py
@@ -13,9 +13,13 @@
 # limitations under the License.
 
 """metric unit tests."""
+from parameterized import parameterized
+
 import streamlit as st
 from streamlit.errors import StreamlitAPIException
+from streamlit.proto.LabelVisibilityMessage_pb2 import LabelVisibilityMessage
 from streamlit.proto.Metric_pb2 import Metric as MetricProto
+from streamlit.type_util import _LOGGER
 from tests.delta_generator_test_case import DeltaGeneratorTestCase
 
 
@@ -28,6 +32,10 @@ def test_no_value(self):
         self.assertEqual(c.label, "label_test")
         # This is an em dash. Not a regular "-"
         self.assertEqual(c.body, "—")
+        self.assertEqual(
+            c.label_visibility.value,
+            LabelVisibilityMessage.LabelVisibilityOptions.VISIBLE,
+        )
 
     def test_label_and_value(self):
         """Test that metric can be called with label and value passed in."""
@@ -39,6 +47,22 @@ def test_label_and_value(self):
         self.assertEqual(c.color, MetricProto.MetricColor.GRAY)
         self.assertEqual(c.direction, MetricProto.MetricDirection.NONE)
 
+    @parameterized.expand(
+        [
+            ("visible", LabelVisibilityMessage.LabelVisibilityOptions.VISIBLE),
+            ("hidden", LabelVisibilityMessage.LabelVisibilityOptions.HIDDEN),
+            ("collapsed", LabelVisibilityMessage.LabelVisibilityOptions.COLLAPSED),
+        ]
+    )
+    def test_label_visibility(self, label_visibility_value, proto_value):
+        """Test that metric can be called with label_visibility param."""
+        st.metric("label_test", "123", label_visibility=label_visibility_value)
+
+        c = self.get_delta_from_queue().new_element.metric
+        self.assertEqual(c.label, "label_test")
+        self.assertEqual(c.body, "123")
+        self.assertEqual(c.label_visibility.value, proto_value)
+
     def test_label_and_value_and_delta_and_delta_color(self):
         """Test that metric can be called with label, value, delta, and delta
         colors passed in."""
@@ -157,6 +181,26 @@ def test_invalid_label(self):
             str(exc.exception),
         )
 
+    def test_invalid_label_visibility(self):
+        with self.assertRaises(StreamlitAPIException) as e:
+            st.metric("label_test", "123", label_visibility="wrong_value")
+        self.assertEqual(
+            str(e.exception),
+            "Unsupported label_visibility option 'wrong_value'. Valid values are "
+            "'visible', 'hidden' or 'collapsed'.",
+        )
+
+    def test_empty_label_warning(self):
+        """Test that a warning is logged if st.metric was called with empty label."""
+
+        with self.assertLogs(_LOGGER) as logs:
+            st.metric(label="", value="123")
+
+        self.assertIn(
+            "`label` got an empty value. This is discouraged for accessibility reasons",
+            logs.records[0].msg,
+        )
+
     def test_invalid_value(self):
         with self.assertRaises(TypeError) as exc:
             st.metric("Testing", [1, 2, 3])
diff --git a/proto/streamlit/proto/Metric.proto b/proto/streamlit/proto/Metric.proto
index 8d0543344722..f40b684912e1 100644
--- a/proto/streamlit/proto/Metric.proto
+++ b/proto/streamlit/proto/Metric.proto
@@ -16,6 +16,8 @@
 
 syntax = "proto3";
 
+import "streamlit/proto/LabelVisibilityMessage.proto";
+
 message Metric {
   string label = 1;
   string body = 2;
@@ -23,6 +25,7 @@ message Metric {
   MetricDirection direction = 4;
   MetricColor color = 5;
   string help = 6;
+  LabelVisibilityMessage label_visibility = 7;
 
   enum MetricColor{
     RED = 0;


Code review comments:
diff_hunk: @@ -16,20 +16,29 @@
 
 import styled from "@emotion/styled"
 import { StyledWidgetLabel } from "src/components/widgets/BaseWidget/styled-components"
+import { LabelVisibilityOptions } from "src/lib/utils"
 
-export const StyledTruncateText = styled.div(({ theme }) => ({
-  overflowWrap: "normal",
-  textOverflow: "ellipsis",
-  width: "100%",
-  overflow: "hidden",
-  whiteSpace: "nowrap",
-  fontFamily: theme.genericFonts.bodyFont,
-  lineHeight: theme.lineHeights.normal,
-  verticalAlign: "middle",
-  display: "flex",
-  flexDirection: "row",
-  alignItems: "center",
-}))
+export interface StyledTruncateTextProps {
+  visibility?: LabelVisibilityOptions
+}
+
+export const StyledTruncateText = styled.div<StyledTruncateTextProps>(
+  ({ theme, visibility }) => ({
+    overflowWrap: "normal",
+    textOverflow: "ellipsis",
+    width: "100%",
+    overflow: "hidden",
+    whiteSpace: "nowrap",
+    fontFamily: theme.genericFonts.bodyFont,
+    lineHeight: theme.lineHeights.normal,
+    verticalAlign: "middle",
+    display: visibility === LabelVisibilityOptions.Collapsed ? "none" : "flex",
+    visibility:
+      visibility === LabelVisibilityOptions.Hidden ? "hidden" : "visible",
+    flexDirection: "row",
+    alignItems: "center",
+  })
+)
 
 export const StyledMetricLabelText = styled(StyledWidgetLabel)(() => ({
   marginBottom: 0,
path: frontend/src/components/elements/Metric/styled-components.ts
text: @user1:

Could you please add    
```
 display: visibility === LabelVisibilityOptions.Collapsed ? "none" : "flex",
 visibility: visibility === LabelVisibilityOptions.Hidden ? "hidden" : "visible", 
 ```
 to the `StyledMetricLabelText` class (`StyledTruncateText` changes could be reverted )
 
 This should fix an issue with spacing when `Collapsed`.
 
 And big thanks again for this PR!

@author:
Thanks Karen! Fixed on a04ccae.