# Copyright (c) Streamlit Inc. (2018-2022) Snowflake Inc. (2022-2025) # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from __future__ import annotations from dataclasses import dataclass from textwrap import dedent from typing import TYPE_CHECKING, Any, Literal, TypeAlias, cast from streamlit.dataframe_util import OptionSequence, convert_anything_to_list from streamlit.elements.lib.layout_utils import ( Height, LayoutConfig, Width, validate_height, validate_width, ) from streamlit.elements.lib.policies import maybe_raise_label_warnings from streamlit.elements.lib.utils import ( LabelVisibility, 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 AnyNumber, clean_text, from_number if TYPE_CHECKING: from streamlit.delta_generator import DeltaGenerator Value: TypeAlias = AnyNumber | str | None Delta: TypeAlias = AnyNumber | str | None DeltaColor: TypeAlias = Literal["normal", "inverse", "off"] @dataclass(frozen=True) class MetricColorAndDirection: color: MetricProto.MetricColor.ValueType direction: MetricProto.MetricDirection.ValueType class MetricMixin: @gather_metrics("metric") def metric( self, label: str, value: Value, delta: Delta = None, delta_color: DeltaColor = "normal", *, help: str | None = None, label_visibility: LabelVisibility = "visible", border: bool = False, width: Width = "stretch", height: Height = "content", chart_data: OptionSequence[Any] | None = None, chart_type: Literal["line", "bar", "area"] = "line", ) -> DeltaGenerator: r"""Display a metric in big bold font, with an optional indicator of how the metric changed. Tip: If you want to display a large number, it may be a good idea to shorten it using packages like `millify `_ or `numerize `_. E.g. ``1234`` can be displayed as ``1.2k`` using ``st.metric("Short number", millify(1234))``. Parameters ---------- label : str The header or title for the metric. The label can optionally contain GitHub-flavored Markdown of the following types: Bold, Italics, Strikethroughs, Inline Code, Links, and Images. Images display like icons, with a max height equal to the font height. Unsupported Markdown elements are unwrapped so only their children (text contents) render. Display unsupported elements as literal characters by backslash-escaping them. E.g., ``"1\. Not an ordered list"``. See the ``body`` parameter of |st.markdown|_ for additional, supported Markdown directives. .. |st.markdown| replace:: ``st.markdown`` .. _st.markdown: https://docs.streamlit.io/develop/api-reference/text/st.markdown value : int, float, decimal.Decimal, str, or None Value of the metric. ``None`` is rendered as a long dash. delta : int, float, decimal.Decimal, str, or None Indicator of how the metric changed, rendered with an arrow below the metric. If delta is negative (int/float) or starts with a minus sign (str), the arrow points down and the text is red; else the arrow points up and the text is green. If None (default), no delta indicator is shown. delta_color : "normal", "inverse", or "off" If "normal" (default), the delta indicator is shown as described above. If "inverse", it is red when positive and green when negative. This is useful when a negative change is considered good, e.g. if cost decreased. If "off", delta is shown in gray regardless of its value. help : str or None A tooltip that gets displayed next to the metric label. Streamlit only displays the tooltip when ``label_visibility="visible"``. If this is ``None`` (default), no tooltip is displayed. The tooltip can optionally contain GitHub-flavored Markdown, including the Markdown directives described in the ``body`` parameter of ``st.markdown``. label_visibility : "visible", "hidden", or "collapsed" The visibility of the label. The default is ``"visible"``. If this is ``"hidden"``, Streamlit displays an empty spacer instead of the label, which can help keep the widget aligned with other widgets. If this is ``"collapsed"``, Streamlit displays no label or spacer. border : bool Whether to show a border around the metric container. If this is ``False`` (default), no border is shown. If this is ``True``, a border is shown. height : "content", "stretch", or int The height of the metric element. This can be one of the following: - ``"content"`` (default): The height of the element matches the height of its content. - ``"stretch"``: The height of the element matches the height of its content or the height of the parent container, whichever is larger. If the element is not in a parent container, the height of the element matches the height of its content. - An integer specifying the height in pixels: The element has a fixed height. If the content is larger than the specified height, scrolling is enabled. width : "stretch", "content", or int The width of the metric element. This can be one of the following: - ``"stretch"`` (default): The width of the element matches the width of the parent container. - ``"content"``: The width of the element matches the width of its content, but doesn't exceed the width of the parent container. - An integer specifying the width in pixels: The element has a fixed width. If the specified width is greater than the width of the parent container, the width of the element matches the width of the parent container. chart_data : Iterable or None A sequence of numeric values to display as a sparkline chart. If this is ``None`` (default), no chart is displayed. The sequence can be anything supported by ``st.dataframe``, including a ``list`` or ``set``. If the sequence is dataframe-like, the first column will be used. Each value will be cast to ``float`` internally by default. chart_type : "line", "bar", or "area" The type of sparkline chart to display. This can be one of the following: - ``"line"`` (default): A simple sparkline. - ``"area"``: A sparkline with area shading. - ``"bar"``: A bar chart. Examples -------- **Example 1: Show a metric** >>> import streamlit as st >>> >>> st.metric(label="Temperature", value="70 °F", delta="1.2 °F") .. output:: https://doc-metric-example1.streamlit.app/ height: 210px **Example 2: Create a row of metrics** ``st.metric`` looks especially nice in combination with ``st.columns``. >>> import streamlit as st >>> >>> col1, col2, col3 = st.columns(3) >>> col1.metric("Temperature", "70 °F", "1.2 °F") >>> col2.metric("Wind", "9 mph", "-8%") >>> col3.metric("Humidity", "86%", "4%") .. output:: https://doc-metric-example2.streamlit.app/ height: 210px **Example 3: Modify the delta indicator** The delta indicator color can also be inverted or turned off. >>> import streamlit as st >>> >>> st.metric(label="Gas price", value=4, delta=-0.5, delta_color="inverse") >>> >>> st.metric( ... label="Active developers", ... value=123, ... delta=123, ... delta_color="off", ... ) .. output:: https://doc-metric-example3.streamlit.app/ height: 320px **Example 4: Create a grid of metric cards** Add borders to your metrics to create a dashboard look. >>> import streamlit as st >>> >>> a, b = st.columns(2) >>> c, d = st.columns(2) >>> >>> a.metric("Temperature", "30°F", "-9°F", border=True) >>> b.metric("Wind", "4 mph", "2 mph", border=True) >>> >>> c.metric("Humidity", "77%", "5%", border=True) >>> d.metric("Pressure", "30.34 inHg", "-2 inHg", border=True) .. output:: https://doc-metric-example4.streamlit.app/ height: 350px **Example 5: Show sparklines** To show trends over time, add sparklines. >>> import streamlit as st >>> from numpy.random import default_rng as rng >>> >>> changes = list(rng(4).standard_normal(20)) >>> data = [sum(changes[:i]) for i in range(20)] >>> delta = round(data[-1], 2) >>> >>> row = st.container(horizontal=True) >>> with row: >>> st.metric( ... "Line", 10, delta, chart_data=data, chart_type="line", border=True ... ) >>> st.metric( ... "Area", 10, delta, chart_data=data, chart_type="area", border=True ... ) >>> st.metric( ... "Bar", 10, delta, chart_data=data, chart_type="bar", border=True ... ) .. output:: https://doc-metric-example5.streamlit.app/ height: 300px """ maybe_raise_label_warnings(label, label_visibility) metric_proto = MetricProto() metric_proto.body = _parse_value(value) metric_proto.label = _parse_label(label) metric_proto.delta = _parse_delta(delta) metric_proto.show_border = border if help is not None: metric_proto.help = dedent(help) color_and_direction = _determine_delta_color_and_direction( cast("DeltaColor", clean_text(delta_color)), delta ) 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 ) if chart_data is not None: prepared_data: list[float] = [] for val in convert_anything_to_list(chart_data): try: prepared_data.append(float(val)) except Exception as ex: # noqa: PERF203 raise StreamlitAPIException( "Only numeric values are supported for chart data sequence. The " f"value '{val}' is of type {type(val)} and " "cannot be converted to float." ) from ex if len(prepared_data) > 0: metric_proto.chart_data.extend(prepared_data) metric_proto.chart_type = _parse_chart_type(chart_type) validate_height(height, allow_content=True) validate_width(width, allow_content=True) layout_config = LayoutConfig(width=width, height=height) return self.dg._enqueue("metric", metric_proto, layout_config=layout_config) @property def dg(self) -> DeltaGenerator: return cast("DeltaGenerator", self) def _parse_chart_type( chart_type: Literal["line", "bar", "area"], ) -> MetricProto.ChartType.ValueType: if chart_type == "bar": return MetricProto.ChartType.BAR if chart_type == "area": return MetricProto.ChartType.AREA # Use line as default chart: return MetricProto.ChartType.LINE def _parse_label(label: str) -> str: if not isinstance(label, str): raise TypeError( f"'{label}' is of type {type(label)}, which is not an accepted type." " label only accepts: str. Please convert the label to an accepted type." ) return label def _parse_value(value: Value) -> str: if value is None: return "—" if isinstance(value, str): return value return from_number(value) def _parse_delta(delta: Delta) -> str: if delta is None or delta == "": return "" if isinstance(delta, str): return dedent(delta) return from_number(delta) def _determine_delta_color_and_direction( delta_color: DeltaColor, delta: Delta, ) -> MetricColorAndDirection: if delta_color not in {"normal", "inverse", "off"}: raise StreamlitAPIException( f"'{delta_color}' is not an accepted value. delta_color only accepts: " "'normal', 'inverse', or 'off'" ) if delta is None or delta == "": return MetricColorAndDirection( color=MetricProto.MetricColor.GRAY, direction=MetricProto.MetricDirection.NONE, ) if _is_negative_delta(delta): if delta_color == "normal": cd_color = MetricProto.MetricColor.RED elif delta_color == "inverse": cd_color = MetricProto.MetricColor.GREEN else: cd_color = MetricProto.MetricColor.GRAY cd_direction = MetricProto.MetricDirection.DOWN else: if delta_color == "normal": cd_color = MetricProto.MetricColor.GREEN elif delta_color == "inverse": cd_color = MetricProto.MetricColor.RED else: cd_color = MetricProto.MetricColor.GRAY cd_direction = MetricProto.MetricDirection.UP return MetricColorAndDirection( color=cd_color, direction=cd_direction, ) def _is_negative_delta(delta: Delta) -> bool: return dedent(str(delta)).startswith("-")