593 lines
21 KiB
Python
593 lines
21 KiB
Python
# 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.
|
|
|
|
"""Streamlit support for Plotly charts."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
from dataclasses import dataclass
|
|
from typing import (
|
|
TYPE_CHECKING,
|
|
Any,
|
|
Final,
|
|
Literal,
|
|
TypedDict,
|
|
Union,
|
|
cast,
|
|
overload,
|
|
)
|
|
|
|
from typing_extensions import Required, TypeAlias
|
|
|
|
from streamlit import type_util
|
|
from streamlit.deprecation_util import show_deprecation_warning
|
|
from streamlit.elements.lib.form_utils import current_form_id
|
|
from streamlit.elements.lib.policies import check_widget_policies
|
|
from streamlit.elements.lib.streamlit_plotly_theme import (
|
|
configure_streamlit_plotly_theme,
|
|
)
|
|
from streamlit.elements.lib.utils import Key, compute_and_register_element_id, to_key
|
|
from streamlit.errors import StreamlitAPIException
|
|
from streamlit.proto.PlotlyChart_pb2 import PlotlyChart as PlotlyChartProto
|
|
from streamlit.runtime.metrics_util import gather_metrics
|
|
from streamlit.runtime.scriptrunner_utils.script_run_context import get_script_run_ctx
|
|
from streamlit.runtime.state import WidgetCallback, register_widget
|
|
from streamlit.util import AttributeDictionary
|
|
|
|
if TYPE_CHECKING:
|
|
from collections.abc import Iterable
|
|
|
|
import matplotlib as mpl
|
|
import plotly.graph_objs as go
|
|
from plotly.basedatatypes import BaseFigure
|
|
|
|
from streamlit.delta_generator import DeltaGenerator
|
|
|
|
# We need to configure the Plotly theme before any Plotly figures are created:
|
|
configure_streamlit_plotly_theme()
|
|
|
|
_AtomicFigureOrData: TypeAlias = Union[
|
|
"go.Figure",
|
|
"go.Data",
|
|
]
|
|
FigureOrData: TypeAlias = Union[
|
|
_AtomicFigureOrData,
|
|
list[_AtomicFigureOrData],
|
|
# It is kind of hard to figure out exactly what kind of dict is supported
|
|
# here, as plotly hasn't embraced typing yet. This version is chosen to
|
|
# align with the docstring.
|
|
dict[str, _AtomicFigureOrData],
|
|
"BaseFigure",
|
|
"mpl.figure.Figure",
|
|
]
|
|
|
|
SelectionMode: TypeAlias = Literal["lasso", "points", "box"]
|
|
_SELECTION_MODES: Final[set[SelectionMode]] = {"lasso", "points", "box"}
|
|
|
|
|
|
class PlotlySelectionState(TypedDict, total=False):
|
|
"""
|
|
The schema for the Plotly chart selection state.
|
|
|
|
The selection state is stored in a dictionary-like object that supports both
|
|
key and attribute notation. Selection states cannot be programmatically
|
|
changed or set through Session State.
|
|
|
|
Attributes
|
|
----------
|
|
points : list[dict[str, Any]]
|
|
The selected data points in the chart, including the data points
|
|
selected by the box and lasso mode. The data includes the values
|
|
associated to each point and a point index used to populate
|
|
``point_indices``. If additional information has been assigned to your
|
|
points, such as size or legend group, this is also included.
|
|
|
|
point_indices : list[int]
|
|
The numerical indices of all selected data points in the chart. The
|
|
details of each identified point are included in ``points``.
|
|
|
|
box : list[dict[str, Any]]
|
|
The metadata related to the box selection. This includes the
|
|
coordinates of the selected area.
|
|
|
|
lasso : list[dict[str, Any]]
|
|
The metadata related to the lasso selection. This includes the
|
|
coordinates of the selected area.
|
|
|
|
Example
|
|
-------
|
|
When working with more complicated graphs, the ``points`` attribute
|
|
displays additional information. Try selecting points in the following
|
|
example:
|
|
|
|
>>> import streamlit as st
|
|
>>> import plotly.express as px
|
|
>>>
|
|
>>> df = px.data.iris()
|
|
>>> fig = px.scatter(
|
|
... df,
|
|
... x="sepal_width",
|
|
... y="sepal_length",
|
|
... color="species",
|
|
... size="petal_length",
|
|
... hover_data=["petal_width"],
|
|
... )
|
|
>>>
|
|
>>> event = st.plotly_chart(fig, key="iris", on_select="rerun")
|
|
>>>
|
|
>>> event.selection
|
|
|
|
.. output::
|
|
https://doc-chart-events-plotly-selection-state.streamlit.app
|
|
height: 600px
|
|
|
|
This is an example of the selection state when selecting a single point:
|
|
|
|
>>> {
|
|
>>> "points": [
|
|
>>> {
|
|
>>> "curve_number": 2,
|
|
>>> "point_number": 9,
|
|
>>> "point_index": 9,
|
|
>>> "x": 3.6,
|
|
>>> "y": 7.2,
|
|
>>> "customdata": [
|
|
>>> 2.5
|
|
>>> ],
|
|
>>> "marker_size": 6.1,
|
|
>>> "legendgroup": "virginica"
|
|
>>> }
|
|
>>> ],
|
|
>>> "point_indices": [
|
|
>>> 9
|
|
>>> ],
|
|
>>> "box": [],
|
|
>>> "lasso": []
|
|
>>> }
|
|
|
|
"""
|
|
|
|
points: Required[list[dict[str, Any]]]
|
|
point_indices: Required[list[int]]
|
|
box: Required[list[dict[str, Any]]]
|
|
lasso: Required[list[dict[str, Any]]]
|
|
|
|
|
|
class PlotlyState(TypedDict, total=False):
|
|
"""
|
|
The schema for the Plotly chart event state.
|
|
|
|
The event state is stored in a dictionary-like object that supports both
|
|
key and attribute notation. Event states cannot be programmatically
|
|
changed or set through Session State.
|
|
|
|
Only selection events are supported at this time.
|
|
|
|
Attributes
|
|
----------
|
|
selection : dict
|
|
The state of the ``on_select`` event. This attribute returns a
|
|
dictionary-like object that supports both key and attribute notation.
|
|
The attributes are described by the ``PlotlySelectionState`` dictionary
|
|
schema.
|
|
|
|
Example
|
|
-------
|
|
Try selecting points by any of the three available methods (direct click,
|
|
box, or lasso). The current selection state is available through Session
|
|
State or as the output of the chart function.
|
|
|
|
>>> import streamlit as st
|
|
>>> import plotly.express as px
|
|
>>>
|
|
>>> df = px.data.iris() # iris is a pandas DataFrame
|
|
>>> fig = px.scatter(df, x="sepal_width", y="sepal_length")
|
|
>>>
|
|
>>> event = st.plotly_chart(fig, key="iris", on_select="rerun")
|
|
>>>
|
|
>>> event
|
|
|
|
.. output::
|
|
https://doc-chart-events-plotly-state.streamlit.app
|
|
height: 600px
|
|
|
|
"""
|
|
|
|
selection: Required[PlotlySelectionState]
|
|
|
|
|
|
@dataclass
|
|
class PlotlyChartSelectionSerde:
|
|
"""PlotlyChartSelectionSerde is used to serialize and deserialize the Plotly Chart
|
|
selection state.
|
|
"""
|
|
|
|
def deserialize(self, ui_value: str | None) -> PlotlyState:
|
|
empty_selection_state: PlotlyState = {
|
|
"selection": {
|
|
"points": [],
|
|
"point_indices": [],
|
|
"box": [],
|
|
"lasso": [],
|
|
},
|
|
}
|
|
|
|
selection_state = (
|
|
empty_selection_state
|
|
if ui_value is None
|
|
else cast("PlotlyState", AttributeDictionary(json.loads(ui_value)))
|
|
)
|
|
|
|
if "selection" not in selection_state:
|
|
selection_state = empty_selection_state # type: ignore[unreachable]
|
|
|
|
return cast("PlotlyState", AttributeDictionary(selection_state))
|
|
|
|
def serialize(self, selection_state: PlotlyState) -> str:
|
|
return json.dumps(selection_state, default=str)
|
|
|
|
|
|
def parse_selection_mode(
|
|
selection_mode: SelectionMode | Iterable[SelectionMode],
|
|
) -> set[PlotlyChartProto.SelectionMode.ValueType]:
|
|
"""Parse and check the user provided selection modes."""
|
|
if isinstance(selection_mode, str):
|
|
# Only a single selection mode was passed
|
|
selection_mode_set = {selection_mode}
|
|
else:
|
|
# Multiple selection modes were passed
|
|
selection_mode_set = set(selection_mode)
|
|
|
|
if not selection_mode_set.issubset(_SELECTION_MODES):
|
|
raise StreamlitAPIException(
|
|
f"Invalid selection mode: {selection_mode}. "
|
|
f"Valid options are: {_SELECTION_MODES}"
|
|
)
|
|
|
|
parsed_selection_modes = []
|
|
for mode in selection_mode_set:
|
|
if mode == "points":
|
|
parsed_selection_modes.append(PlotlyChartProto.SelectionMode.POINTS)
|
|
elif mode == "lasso":
|
|
parsed_selection_modes.append(PlotlyChartProto.SelectionMode.LASSO)
|
|
elif mode == "box":
|
|
parsed_selection_modes.append(PlotlyChartProto.SelectionMode.BOX)
|
|
return set(parsed_selection_modes)
|
|
|
|
|
|
class PlotlyMixin:
|
|
@overload
|
|
def plotly_chart(
|
|
self,
|
|
figure_or_data: FigureOrData,
|
|
use_container_width: bool = True,
|
|
*,
|
|
theme: Literal["streamlit"] | None = "streamlit",
|
|
key: Key | None = None,
|
|
on_select: Literal["ignore"], # No default value here to make it work with mypy
|
|
selection_mode: SelectionMode | Iterable[SelectionMode] = (
|
|
"points",
|
|
"box",
|
|
"lasso",
|
|
),
|
|
**kwargs: Any,
|
|
) -> DeltaGenerator: ...
|
|
|
|
@overload
|
|
def plotly_chart(
|
|
self,
|
|
figure_or_data: FigureOrData,
|
|
use_container_width: bool = True,
|
|
*,
|
|
theme: Literal["streamlit"] | None = "streamlit",
|
|
key: Key | None = None,
|
|
on_select: Literal["rerun"] | WidgetCallback = "rerun",
|
|
selection_mode: SelectionMode | Iterable[SelectionMode] = (
|
|
"points",
|
|
"box",
|
|
"lasso",
|
|
),
|
|
**kwargs: Any,
|
|
) -> PlotlyState: ...
|
|
|
|
@gather_metrics("plotly_chart")
|
|
def plotly_chart(
|
|
self,
|
|
figure_or_data: FigureOrData,
|
|
use_container_width: bool = True,
|
|
*,
|
|
theme: Literal["streamlit"] | None = "streamlit",
|
|
key: Key | None = None,
|
|
on_select: Literal["rerun", "ignore"] | WidgetCallback = "ignore",
|
|
selection_mode: SelectionMode | Iterable[SelectionMode] = (
|
|
"points",
|
|
"box",
|
|
"lasso",
|
|
),
|
|
**kwargs: Any,
|
|
) -> DeltaGenerator | PlotlyState:
|
|
"""Display an interactive Plotly chart.
|
|
|
|
`Plotly <https://plot.ly/python>`_ is a charting library for Python.
|
|
The arguments to this function closely follow the ones for Plotly's
|
|
``plot()`` function.
|
|
|
|
To show Plotly charts in Streamlit, call ``st.plotly_chart`` wherever
|
|
you would call Plotly's ``py.plot`` or ``py.iplot``.
|
|
|
|
.. Important::
|
|
You must install ``plotly`` to use this command. Your app's
|
|
performance may be enhanced by installing ``orjson`` as well.
|
|
|
|
Parameters
|
|
----------
|
|
figure_or_data : plotly.graph_objs.Figure, plotly.graph_objs.Data,\
|
|
or dict/list of plotly.graph_objs.Figure/Data
|
|
|
|
The Plotly ``Figure`` or ``Data`` object to render. See
|
|
https://plot.ly/python/ for examples of graph descriptions.
|
|
|
|
.. note::
|
|
If your chart contains more than 1000 data points, Plotly will
|
|
use a WebGL renderer to display the chart. Different browsers
|
|
have different limits on the number of WebGL contexts per page.
|
|
If you have multiple WebGL contexts on a page, you may need to
|
|
switch to SVG rendering mode. You can do this by setting
|
|
``render_mode="svg"`` within the figure. For example, the
|
|
following code defines a Plotly Express line chart that will
|
|
render in SVG mode when passed to ``st.plotly_chart``:
|
|
``px.line(df, x="x", y="y", render_mode="svg")``.
|
|
|
|
use_container_width : bool
|
|
Whether to override the figure's native width with the width of
|
|
the parent container. If ``use_container_width`` is ``True`` (default),
|
|
Streamlit sets the width of the figure to match the width of the parent
|
|
container. If ``use_container_width`` is ``False``, Streamlit sets the
|
|
width of the chart to fit its contents according to the plotting library,
|
|
up to the width of the parent container.
|
|
|
|
theme : "streamlit" or None
|
|
The theme of the chart. If ``theme`` is ``"streamlit"`` (default),
|
|
Streamlit uses its own design default. If ``theme`` is ``None``,
|
|
Streamlit falls back to the default behavior of the library.
|
|
|
|
The ``"streamlit"`` theme can be partially customized through the
|
|
configuration options ``theme.chartCategoricalColors`` and
|
|
``theme.chartSequentialColors``. Font configuration options are
|
|
also applied.
|
|
|
|
key : str
|
|
An optional string to use for giving this element a stable
|
|
identity. If ``key`` is ``None`` (default), this element's identity
|
|
will be determined based on the values of the other parameters.
|
|
|
|
Additionally, if selections are activated and ``key`` is provided,
|
|
Streamlit will register the key in Session State to store the
|
|
selection state. The selection state is read-only.
|
|
|
|
on_select : "ignore" or "rerun" or callable
|
|
How the figure should respond to user selection events. This
|
|
controls whether or not the figure behaves like an input widget.
|
|
``on_select`` can be one of the following:
|
|
|
|
- ``"ignore"`` (default): Streamlit will not react to any selection
|
|
events in the chart. The figure will not behave like an input
|
|
widget.
|
|
|
|
- ``"rerun"``: Streamlit will rerun the app when the user selects
|
|
data in the chart. In this case, ``st.plotly_chart`` will return
|
|
the selection data as a dictionary.
|
|
|
|
- A ``callable``: Streamlit will rerun the app and execute the
|
|
``callable`` as a callback function before the rest of the app.
|
|
In this case, ``st.plotly_chart`` will return the selection data
|
|
as a dictionary.
|
|
|
|
selection_mode : "points", "box", "lasso" or an Iterable of these
|
|
The selection mode of the chart. This can be one of the following:
|
|
|
|
- ``"points"``: The chart will allow selections based on individual
|
|
data points.
|
|
- ``"box"``: The chart will allow selections based on rectangular
|
|
areas.
|
|
- ``"lasso"``: The chart will allow selections based on freeform
|
|
areas.
|
|
- An ``Iterable`` of the above options: The chart will allow
|
|
selections based on the modes specified.
|
|
|
|
All selections modes are activated by default.
|
|
|
|
**kwargs
|
|
Additional arguments accepted by Plotly's ``plot()`` function.
|
|
|
|
This supports ``config``, a dictionary of Plotly configuration
|
|
options. For more information about Plotly configuration options,
|
|
see Plotly's documentation on `Configuration in Python
|
|
<https://plotly.com/python/configuration-options/>`_.
|
|
|
|
Returns
|
|
-------
|
|
element or dict
|
|
If ``on_select`` is ``"ignore"`` (default), this command returns an
|
|
internal placeholder for the chart element. Otherwise, this command
|
|
returns a dictionary-like object that supports both key and
|
|
attribute notation. The attributes are described by the
|
|
``PlotlyState`` dictionary schema.
|
|
|
|
Examples
|
|
--------
|
|
**Example 1: Basic Plotly Chart**
|
|
|
|
The example below comes from the examples at https://plot.ly/python.
|
|
Note that ``plotly.figure_factory`` requires ``scipy`` to run.
|
|
|
|
>>> import streamlit as st
|
|
>>> import numpy as np
|
|
>>> import plotly.figure_factory as ff
|
|
>>>
|
|
>>> # Add histogram data
|
|
>>> x1 = np.random.randn(200) - 2
|
|
>>> x2 = np.random.randn(200)
|
|
>>> x3 = np.random.randn(200) + 2
|
|
>>>
|
|
>>> # Group data together
|
|
>>> hist_data = [x1, x2, x3]
|
|
>>>
|
|
>>> group_labels = ['Group 1', 'Group 2', 'Group 3']
|
|
>>>
|
|
>>> # Create distplot with custom bin_size
|
|
>>> fig = ff.create_distplot(
|
|
... hist_data, group_labels, bin_size=[.1, .25, .5])
|
|
>>>
|
|
>>> # Plot!
|
|
>>> st.plotly_chart(fig)
|
|
|
|
.. output::
|
|
https://doc-plotly-chart.streamlit.app/
|
|
height: 550px
|
|
|
|
**Example 2: Plotly Chart with configuration**
|
|
|
|
By default, Plotly charts have scroll zoom enabled. If you have a
|
|
longer page and want to avoid conflicts between page scrolling and
|
|
zooming, you can use Plotly's configuration options to disable scroll
|
|
zoom. In the following example, scroll zoom is disabled, but the zoom
|
|
buttons are still enabled in the modebar.
|
|
|
|
>>> import streamlit as st
|
|
>>> import plotly.graph_objects as go
|
|
>>>
|
|
>>> fig = go.Figure()
|
|
>>> fig.add_trace(
|
|
... go.Scatter(
|
|
... x=[1, 2, 3, 4, 5],
|
|
... y=[1, 3, 2, 5, 4]
|
|
... )
|
|
... )
|
|
>>>
|
|
>>> st.plotly_chart(fig, config = {'scrollZoom': False})
|
|
|
|
.. output::
|
|
https://doc-plotly-chart-config.streamlit.app/
|
|
height: 550px
|
|
|
|
"""
|
|
import plotly.io
|
|
import plotly.tools
|
|
|
|
# NOTE: "figure_or_data" is the name used in Plotly's .plot() method
|
|
# for their main parameter. I don't like the name, but it's best to
|
|
# keep it in sync with what Plotly calls it.
|
|
|
|
if "sharing" in kwargs:
|
|
show_deprecation_warning(
|
|
"The `sharing` parameter has been deprecated and will be removed "
|
|
"in a future release. Plotly charts will always be rendered using "
|
|
"Streamlit's offline mode."
|
|
)
|
|
|
|
if theme not in ["streamlit", None]:
|
|
raise StreamlitAPIException(
|
|
f'You set theme="{theme}" while Streamlit charts only support '
|
|
"theme=”streamlit” or theme=None to fallback to the default "
|
|
"library theme."
|
|
)
|
|
|
|
if on_select not in ["ignore", "rerun"] and not callable(on_select):
|
|
raise StreamlitAPIException(
|
|
f"You have passed {on_select} to `on_select`. But only 'ignore', "
|
|
"'rerun', or a callable is supported."
|
|
)
|
|
|
|
key = to_key(key)
|
|
is_selection_activated = on_select != "ignore"
|
|
|
|
if is_selection_activated:
|
|
# Run some checks that are only relevant when selections are activated
|
|
|
|
is_callback = callable(on_select)
|
|
check_widget_policies(
|
|
self.dg,
|
|
key,
|
|
on_change=cast("WidgetCallback", on_select) if is_callback else None,
|
|
default_value=None,
|
|
writes_allowed=False,
|
|
enable_check_callback_rules=is_callback,
|
|
)
|
|
|
|
if type_util.is_type(figure_or_data, "matplotlib.figure.Figure"):
|
|
# Convert matplotlib figure to plotly figure:
|
|
figure = plotly.tools.mpl_to_plotly(figure_or_data)
|
|
else:
|
|
figure = plotly.tools.return_figure_from_figure_or_data(
|
|
figure_or_data, validate_figure=True
|
|
)
|
|
|
|
plotly_chart_proto = PlotlyChartProto()
|
|
plotly_chart_proto.use_container_width = use_container_width
|
|
plotly_chart_proto.theme = theme or ""
|
|
plotly_chart_proto.form_id = current_form_id(self.dg)
|
|
|
|
config = dict(kwargs.get("config", {}))
|
|
# Copy over some kwargs to config dict. Plotly does the same in plot().
|
|
config.setdefault("showLink", kwargs.get("show_link", False))
|
|
config.setdefault("linkText", kwargs.get("link_text", False))
|
|
|
|
plotly_chart_proto.spec = plotly.io.to_json(figure, validate=False)
|
|
plotly_chart_proto.config = json.dumps(config)
|
|
|
|
ctx = get_script_run_ctx()
|
|
|
|
# We are computing the widget id for all plotly uses
|
|
# to also allow non-widget Plotly charts to keep their state
|
|
# when the frontend component gets unmounted and remounted.
|
|
plotly_chart_proto.id = compute_and_register_element_id(
|
|
"plotly_chart",
|
|
user_key=key,
|
|
form_id=plotly_chart_proto.form_id,
|
|
dg=self.dg,
|
|
plotly_spec=plotly_chart_proto.spec,
|
|
plotly_config=plotly_chart_proto.config,
|
|
selection_mode=selection_mode,
|
|
is_selection_activated=is_selection_activated,
|
|
theme=theme,
|
|
use_container_width=use_container_width,
|
|
)
|
|
|
|
if is_selection_activated:
|
|
# Selections are activated, treat plotly chart as a widget:
|
|
plotly_chart_proto.selection_mode.extend(
|
|
parse_selection_mode(selection_mode)
|
|
)
|
|
|
|
serde = PlotlyChartSelectionSerde()
|
|
|
|
widget_state = register_widget(
|
|
plotly_chart_proto.id,
|
|
on_change_handler=on_select if callable(on_select) else None,
|
|
deserializer=serde.deserialize,
|
|
serializer=serde.serialize,
|
|
ctx=ctx,
|
|
value_type="string_value",
|
|
)
|
|
|
|
self.dg._enqueue("plotly_chart", plotly_chart_proto)
|
|
return widget_state.value
|
|
return self.dg._enqueue("plotly_chart", plotly_chart_proto)
|
|
|
|
@property
|
|
def dg(self) -> DeltaGenerator:
|
|
"""Get our DeltaGenerator."""
|
|
return cast("DeltaGenerator", self)
|