first commit

This commit is contained in:
Ayxan
2022-05-23 00:16:32 +04:00
commit d660f2a4ca
24786 changed files with 4428337 additions and 0 deletions

View File

@@ -0,0 +1,13 @@
# Copyright 2018-2022 Streamlit Inc.
#
# 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.

View File

@@ -0,0 +1,98 @@
# Copyright 2018-2022 Streamlit Inc.
#
# 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 typing import cast
import streamlit
from streamlit.proto.Alert_pb2 import Alert as AlertProto
from .utils import clean_text
class AlertMixin:
def error(self, body):
"""Display error message.
Parameters
----------
body : str
The error text to display.
Example
-------
>>> st.error('This is an error')
"""
alert_proto = AlertProto()
alert_proto.body = clean_text(body)
alert_proto.format = AlertProto.ERROR
return self.dg._enqueue("alert", alert_proto)
def warning(self, body):
"""Display warning message.
Parameters
----------
body : str
The warning text to display.
Example
-------
>>> st.warning('This is a warning')
"""
alert_proto = AlertProto()
alert_proto.body = clean_text(body)
alert_proto.format = AlertProto.WARNING
return self.dg._enqueue("alert", alert_proto)
def info(self, body):
"""Display an informational message.
Parameters
----------
body : str
The info text to display.
Example
-------
>>> st.info('This is a purely informational message')
"""
alert_proto = AlertProto()
alert_proto.body = clean_text(body)
alert_proto.format = AlertProto.INFO
return self.dg._enqueue("alert", alert_proto)
def success(self, body):
"""Display a success message.
Parameters
----------
body : str
The success text to display.
Example
-------
>>> st.success('This is a success message!')
"""
alert_proto = AlertProto()
alert_proto.body = clean_text(body)
alert_proto.format = AlertProto.SUCCESS
return self.dg._enqueue("alert", alert_proto)
@property
def dg(self) -> "streamlit.delta_generator.DeltaGenerator":
"""Get our DeltaGenerator."""
return cast("streamlit.delta_generator.DeltaGenerator", self)

View File

@@ -0,0 +1,406 @@
# Copyright 2018-2022 Streamlit Inc.
#
# 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 collections.abc import Iterable
from typing import Any, Dict, List, Optional, Union, cast
from numpy import ndarray
from pandas import DataFrame
from pandas.io.formats.style import Styler
import pyarrow as pa
import streamlit
from streamlit import type_util
from streamlit.proto.Arrow_pb2 import Arrow as ArrowProto
Data = Optional[
Union[DataFrame, Styler, pa.Table, ndarray, Iterable, Dict[str, List[Any]]]
]
class ArrowMixin:
def _arrow_dataframe(
self,
data: Data = None,
width: Optional[int] = None,
height: Optional[int] = None,
) -> "streamlit.delta_generator.DeltaGenerator":
"""Display a dataframe as an interactive table.
Parameters
----------
data : pandas.DataFrame, pandas.Styler, pyarrow.Table, numpy.ndarray, Iterable, dict, or None
The data to display.
If 'data' is a pandas.Styler, it will be used to style its
underyling DataFrame.
width : int or None
Desired width of the UI element expressed in pixels. If None, a
default width based on the page width is used.
height : int or None
Desired height of the UI element expressed in pixels. If None, a
default height is used.
Examples
--------
>>> df = pd.DataFrame(
... np.random.randn(50, 20),
... columns=('col %d' % i for i in range(20)))
...
>>> st._arrow_dataframe(df)
>>> st._arrow_dataframe(df, 200, 100)
You can also pass a Pandas Styler object to change the style of
the rendered DataFrame:
>>> df = pd.DataFrame(
... np.random.randn(10, 20),
... columns=('col %d' % i for i in range(20)))
...
>>> st._arrow_dataframe(df.style.highlight_max(axis=0))
"""
# If pandas.Styler uuid is not provided, a hash of the position
# of the element will be used. This will cause a rerender of the table
# when the position of the element is changed.
delta_path = self.dg._get_delta_path_str()
default_uuid = str(hash(delta_path))
proto = ArrowProto()
marshall(proto, data, default_uuid)
return cast(
"streamlit.delta_generator.DeltaGenerator",
self.dg._enqueue(
"arrow_data_frame", proto, element_width=width, element_height=height
),
)
def _arrow_table(
self, data: Data = None
) -> "streamlit.delta_generator.DeltaGenerator":
"""Display a static table.
This differs from `st._arrow_dataframe` in that the table in this case is
static: its entire contents are laid out directly on the page.
Parameters
----------
data : pandas.DataFrame, pandas.Styler, pyarrow.Table, numpy.ndarray, Iterable, dict, or None
The table data.
Example
-------
>>> df = pd.DataFrame(
... np.random.randn(10, 5),
... columns=("col %d" % i for i in range(5)))
...
>>> st._arrow_table(df)
"""
# If pandas.Styler uuid is not provided, a hash of the position
# of the element will be used. This will cause a rerender of the table
# when the position of the element is changed.
delta_path = self.dg._get_delta_path_str()
default_uuid = str(hash(delta_path))
proto = ArrowProto()
marshall(proto, data, default_uuid)
return cast(
"streamlit.delta_generator.DeltaGenerator",
self.dg._enqueue("arrow_table", proto),
)
@property
def dg(self) -> "streamlit.delta_generator.DeltaGenerator":
"""Get our DeltaGenerator."""
return cast("streamlit.delta_generator.DeltaGenerator", self)
def marshall(proto: ArrowProto, data: Data, default_uuid: Optional[str] = None) -> None:
"""Marshall pandas.DataFrame into an Arrow proto.
Parameters
----------
proto : proto.Arrow
Output. The protobuf for Streamlit Arrow proto.
data : pandas.DataFrame, pandas.Styler, pyarrow.Table, numpy.ndarray, Iterable, dict, or None
Something that is or can be converted to a dataframe.
default_uuid : Optional[str]
If pandas.Styler UUID is not provided, this value will be used.
This attribute is optional and only used for pandas.Styler, other elements
(e.g. charts) can ignore it.
"""
if type_util.is_pandas_styler(data):
# default_uuid is a string only if the data is a `Styler`,
# and `None` otherwise.
assert isinstance(
default_uuid, str
), "Default UUID must be a string for Styler data."
_marshall_styler(proto, data, default_uuid)
if isinstance(data, pa.Table):
proto.data = type_util.pyarrow_table_to_bytes(data)
else:
df = type_util.convert_anything_to_df(data)
proto.data = type_util.data_frame_to_bytes(df)
def _marshall_styler(proto: ArrowProto, styler: Styler, default_uuid: str) -> None:
"""Marshall pandas.Styler into an Arrow proto.
Parameters
----------
proto : proto.Arrow
Output. The protobuf for Streamlit Arrow proto.
styler : pandas.Styler
Helps style a DataFrame or Series according to the data with HTML and CSS.
default_uuid : str
If pandas.Styler uuid is not provided, this value will be used.
"""
# pandas.Styler uuid should be set before _compute is called.
_marshall_uuid(proto, styler, default_uuid)
# We're using protected members of pandas.Styler to get styles,
# which is not ideal and could break if the interface changes.
styler._compute()
# In Pandas 1.3.0, styler._translate() signature was changed.
# 2 arguments were added: sparse_index and sparse_columns.
# The functionality that they provide is not yet supported.
if type_util.is_pandas_version_less_than("1.3.0"):
pandas_styles = styler._translate()
else:
pandas_styles = styler._translate(False, False)
_marshall_caption(proto, styler)
_marshall_styles(proto, styler, pandas_styles)
_marshall_display_values(proto, styler.data, pandas_styles)
def _marshall_uuid(proto: ArrowProto, styler: Styler, default_uuid: str) -> None:
"""Marshall pandas.Styler uuid into an Arrow proto.
Parameters
----------
proto : proto.Arrow
Output. The protobuf for Streamlit Arrow proto.
styler : pandas.Styler
Helps style a DataFrame or Series according to the data with HTML and CSS.
default_uuid : str
If pandas.Styler uuid is not provided, this value will be used.
"""
if styler.uuid is None:
styler.set_uuid(default_uuid)
proto.styler.uuid = str(styler.uuid)
def _marshall_caption(proto: ArrowProto, styler: Styler) -> None:
"""Marshall pandas.Styler caption into an Arrow proto.
Parameters
----------
proto : proto.Arrow
Output. The protobuf for Streamlit Arrow proto.
styler : pandas.Styler
Helps style a DataFrame or Series according to the data with HTML and CSS.
"""
if styler.caption is not None:
proto.styler.caption = styler.caption
def _marshall_styles(proto: ArrowProto, styler: Styler, styles: Dict[str, Any]) -> None:
"""Marshall pandas.Styler styles into an Arrow proto.
Parameters
----------
proto : proto.Arrow
Output. The protobuf for Streamlit Arrow proto.
styler : pandas.Styler
Helps style a DataFrame or Series according to the data with HTML and CSS.
styles : dict
pandas.Styler translated styles.
"""
css_rules = []
if "table_styles" in styles:
table_styles = styles["table_styles"]
table_styles = _trim_pandas_styles(table_styles)
for style in table_styles:
# styles in "table_styles" have a space
# between the uuid and selector.
rule = _pandas_style_to_css(
"table_styles", style, styler.uuid, separator=" "
)
css_rules.append(rule)
if "cellstyle" in styles:
cellstyle = styles["cellstyle"]
cellstyle = _trim_pandas_styles(cellstyle)
for style in cellstyle:
rule = _pandas_style_to_css("cell_style", style, styler.uuid)
css_rules.append(rule)
if len(css_rules) > 0:
proto.styler.styles = "\n".join(css_rules)
def _trim_pandas_styles(styles: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
"""Filter out empty styles.
Every cell will have a class, but the list of props
may just be [['', '']].
Parameters
----------
styles : list
pandas.Styler translated styles.
"""
return [x for x in styles if any(any(y) for y in x["props"])]
def _pandas_style_to_css(
style_type: str,
style: Dict[str, Any],
uuid: str,
separator: str = "",
) -> str:
"""Convert pandas.Styler translated style to CSS.
Parameters
----------
style_type : str
Either "table_styles" or "cell_style".
style : dict
pandas.Styler translated style.
uuid : str
pandas.Styler uuid.
separator : str
A string separator used between table and cell selectors.
"""
declarations = []
for css_property, css_value in style["props"]:
declaration = css_property.strip() + ": " + css_value.strip()
declarations.append(declaration)
table_selector = f"#T_{uuid}"
# In pandas < 1.1.0
# translated_style["cellstyle"] has the following shape:
# [
# {
# "props": [["color", " black"], ["background-color", "orange"], ["", ""]],
# "selector": "row0_col0"
# }
# ...
# ]
#
# In pandas >= 1.1.0
# translated_style["cellstyle"] has the following shape:
# [
# {
# "props": [("color", " black"), ("background-color", "orange"), ("", "")],
# "selectors": ["row0_col0"]
# }
# ...
# ]
if style_type == "table_styles" or (
style_type == "cell_style" and type_util.is_pandas_version_less_than("1.1.0")
):
cell_selectors = [style["selector"]]
else:
cell_selectors = style["selectors"]
selectors = []
for cell_selector in cell_selectors:
selectors.append(table_selector + separator + cell_selector)
selector = ", ".join(selectors)
declaration_block = "; ".join(declarations)
rule_set = selector + " { " + declaration_block + " }"
return rule_set
def _marshall_display_values(
proto: ArrowProto, df: DataFrame, styles: Dict[str, Any]
) -> None:
"""Marshall pandas.Styler display values into an Arrow proto.
Parameters
----------
proto : proto.Arrow
Output. The protobuf for Streamlit Arrow proto.
df : pandas.DataFrame
A dataframe with original values.
styles : dict
pandas.Styler translated styles.
"""
new_df = _use_display_values(df, styles)
proto.styler.display_values = type_util.data_frame_to_bytes(new_df)
def _use_display_values(df: DataFrame, styles: Dict[str, Any]) -> DataFrame:
"""Create a new pandas.DataFrame where display values are used instead of original ones.
Parameters
----------
df : pandas.DataFrame
A dataframe with original values.
styles : dict
pandas.Styler translated styles.
"""
import re
# If values in a column are not of the same type, Arrow
# serialization would fail. Thus, we need to cast all values
# of the dataframe to strings before assigning them display values.
new_df = df.astype(str)
cell_selector_regex = re.compile(r"row(\d+)_col(\d+)")
if "body" in styles:
rows = styles["body"]
for row in rows:
for cell in row:
match = cell_selector_regex.match(cell["id"])
if match:
r, c = map(int, match.groups())
new_df.iat[r, c] = str(cell["display_value"])
return new_df

View File

@@ -0,0 +1,380 @@
# Copyright 2018-2022 Streamlit Inc.
#
# 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.
"""A Python wrapper around Altair.
Altair is a Python visualization library based on Vega-Lite,
a nice JSON schema for expressing graphs and charts."""
from datetime import date
from enum import Enum
from typing import cast
import altair as alt
import pandas as pd
from altair.vegalite.v4.api import Chart
import streamlit
import streamlit.elements.arrow_vega_lite as arrow_vega_lite
from streamlit import type_util
from streamlit.proto.ArrowVegaLiteChart_pb2 import (
ArrowVegaLiteChart as ArrowVegaLiteChartProto,
)
from .arrow import Data
from .utils import last_index_for_melted_dataframes
class ChartType(Enum):
AREA = "area"
BAR = "bar"
LINE = "line"
class ArrowAltairMixin:
def _arrow_line_chart(
self,
data: Data = None,
width: int = 0,
height: int = 0,
use_container_width: bool = True,
) -> "streamlit.delta_generator.DeltaGenerator":
"""Display a line chart.
This is syntax-sugar around st._arrow_altair_chart. The main difference
is this command uses the data's own column and indices to figure out
the chart's spec. As a result this is easier to use for many "just plot
this" scenarios, while being less customizable.
If st._arrow_line_chart does not guess the data specification
correctly, try specifying your desired chart using st._arrow_altair_chart.
Parameters
----------
data : pandas.DataFrame, pandas.Styler, pyarrow.Table, numpy.ndarray, Iterable, dict or None
Data to be plotted.
width : int
The chart width in pixels. If 0, selects the width automatically.
height : int
The chart height in pixels. If 0, selects the height automatically.
use_container_width : bool
If True, set the chart width to the column width. This takes
precedence over the width argument.
Example
-------
>>> chart_data = pd.DataFrame(
... np.random.randn(20, 3),
... columns=['a', 'b', 'c'])
...
>>> st._arrow_line_chart(chart_data)
.. output::
https://static.streamlit.io/0.50.0-td2L/index.html?id=BdxXG3MmrVBfJyqS2R2ki8
height: 220px
"""
proto = ArrowVegaLiteChartProto()
chart = _generate_chart(ChartType.LINE, data, width, height)
marshall(proto, chart, use_container_width)
last_index = last_index_for_melted_dataframes(data)
return cast(
"streamlit.delta_generator.DeltaGenerator",
self.dg._enqueue("arrow_line_chart", proto, last_index=last_index),
)
def _arrow_area_chart(
self,
data: Data = None,
width: int = 0,
height: int = 0,
use_container_width: bool = True,
) -> "streamlit.delta_generator.DeltaGenerator":
"""Display an area chart.
This is just syntax-sugar around st._arrow_altair_chart. The main difference
is this command uses the data's own column and indices to figure out
the chart's spec. As a result this is easier to use for many "just plot
this" scenarios, while being less customizable.
If st._arrow_area_chart does not guess the data specification
correctly, try specifying your desired chart using st._arrow_altair_chart.
Parameters
----------
data : pandas.DataFrame, pandas.Styler, pyarrow.Table, numpy.ndarray, Iterable, or dict
Data to be plotted.
width : int
The chart width in pixels. If 0, selects the width automatically.
height : int
The chart height in pixels. If 0, selects the height automatically.
use_container_width : bool
If True, set the chart width to the column width. This takes
precedence over the width argument.
Example
-------
>>> chart_data = pd.DataFrame(
... np.random.randn(20, 3),
... columns=['a', 'b', 'c'])
...
>>> st._arrow_area_chart(chart_data)
.. output::
https://static.streamlit.io/0.50.0-td2L/index.html?id=Pp65STuFj65cJRDfhGh4Jt
height: 220px
"""
proto = ArrowVegaLiteChartProto()
chart = _generate_chart(ChartType.AREA, data, width, height)
marshall(proto, chart, use_container_width)
last_index = last_index_for_melted_dataframes(data)
return cast(
"streamlit.delta_generator.DeltaGenerator",
self.dg._enqueue("arrow_area_chart", proto, last_index=last_index),
)
def _arrow_bar_chart(
self,
data: Data = None,
width: int = 0,
height: int = 0,
use_container_width: bool = True,
) -> "streamlit.delta_generator.DeltaGenerator":
"""Display a bar chart.
This is just syntax-sugar around st._arrow_altair_chart. The main difference
is this command uses the data's own column and indices to figure out
the chart's spec. As a result this is easier to use for many "just plot
this" scenarios, while being less customizable.
If st._arrow_bar_chart does not guess the data specification
correctly, try specifying your desired chart using st._arrow_altair_chart.
Parameters
----------
data : pandas.DataFrame, pandas.Styler, pyarrow.Table, numpy.ndarray, Iterable, or dict
Data to be plotted.
width : int
The chart width in pixels. If 0, selects the width automatically.
height : int
The chart height in pixels. If 0, selects the height automatically.
use_container_width : bool
If True, set the chart width to the column width. This takes
precedence over the width argument.
Example
-------
>>> chart_data = pd.DataFrame(
... np.random.randn(50, 3),
... columns=["a", "b", "c"])
...
>>> st._arrow_bar_chart(chart_data)
.. output::
https://static.streamlit.io/0.66.0-2BLtg/index.html?id=GaYDn6vxskvBUkBwsGVEaL
height: 220px
"""
proto = ArrowVegaLiteChartProto()
chart = _generate_chart(ChartType.BAR, data, width, height)
marshall(proto, chart, use_container_width)
last_index = last_index_for_melted_dataframes(data)
return cast(
"streamlit.delta_generator.DeltaGenerator",
self.dg._enqueue("arrow_bar_chart", proto, last_index=last_index),
)
def _arrow_altair_chart(
self, altair_chart: Chart, use_container_width: bool = False
) -> "streamlit.delta_generator.DeltaGenerator":
"""Display a chart using the Altair library.
Parameters
----------
altair_chart : altair.vegalite.v2.api.Chart
The Altair chart object to display.
use_container_width : bool
If True, set the chart width to the column width. This takes
precedence over Altair's native `width` value.
Example
-------
>>> import pandas as pd
>>> import numpy as np
>>> import altair as alt
>>>
>>> df = pd.DataFrame(
... np.random.randn(200, 3),
... columns=['a', 'b', 'c'])
...
>>> c = alt.Chart(df).mark_circle().encode(
... x='a', y='b', size='c', color='c', tooltip=['a', 'b', 'c'])
>>>
>>> st._arrow_altair_chart(c, use_container_width=True)
.. output::
https://static.streamlit.io/0.25.0-2JkNY/index.html?id=8jmmXR8iKoZGV4kXaKGYV5
height: 200px
Examples of Altair charts can be found at
https://altair-viz.github.io/gallery/.
"""
proto = ArrowVegaLiteChartProto()
marshall(
proto,
altair_chart,
use_container_width=use_container_width,
)
return cast(
"streamlit.delta_generator.DeltaGenerator",
self.dg._enqueue("arrow_vega_lite_chart", proto),
)
@property
def dg(self) -> "streamlit.delta_generator.DeltaGenerator":
"""Get our DeltaGenerator."""
return cast("streamlit.delta_generator.DeltaGenerator", self)
def _is_date_column(df: pd.DataFrame, name: str) -> bool:
"""True if the column with the given name stores datetime.date values.
This function just checks the first value in the given column, so
it's meaningful only for columns whose values all share the same type.
Parameters
----------
df : pd.DataFrame
name : str
The column name
Returns
-------
bool
"""
column = df[name]
if column.size == 0:
return False
return isinstance(column[0], date)
def _generate_chart(
chart_type: ChartType, data: Data, width: int = 0, height: int = 0
) -> Chart:
"""This function uses the chart's type, data columns and indices to figure out the chart's spec."""
if data is None:
# Use an empty-ish dict because if we use None the x axis labels rotate
# 90 degrees. No idea why. Need to debug.
data = {"": []}
if not isinstance(data, pd.DataFrame):
data = type_util.convert_anything_to_df(data)
index_name = data.index.name
if index_name is None:
index_name = "index"
data = pd.melt(data.reset_index(), id_vars=[index_name])
if chart_type == ChartType.AREA:
opacity = {"value": 0.7}
else:
opacity = {"value": 1.0}
# Set the X and Y axes' scale to "utc" if they contain date values.
# This causes time data to be displayed in UTC, rather the user's local
# time zone. (By default, vega-lite displays time data in the browser's
# local time zone, regardless of which time zone the data specifies:
# https://vega.github.io/vega-lite/docs/timeunit.html#output).
x_scale = (
alt.Scale(type="utc") if _is_date_column(data, index_name) else alt.Undefined
)
y_scale = alt.Scale(type="utc") if _is_date_column(data, "value") else alt.Undefined
x_type = alt.Undefined
# Bar charts should have a discrete (ordinal) x-axis, UNLESS type is date/time
# https://github.com/streamlit/streamlit/pull/2097#issuecomment-714802475
if chart_type == ChartType.BAR and not _is_date_column(data, index_name):
x_type = "ordinal"
chart = (
getattr(
alt.Chart(data, width=width, height=height), "mark_" + chart_type.value
)()
.encode(
alt.X(index_name, title="", scale=x_scale, type=x_type),
alt.Y("value", title="", scale=y_scale),
alt.Color("variable", title="", type="nominal"),
alt.Tooltip([index_name, "value", "variable"]),
opacity=opacity,
)
.interactive()
)
return chart
def marshall(
vega_lite_chart: ArrowVegaLiteChartProto,
altair_chart: Chart,
use_container_width: bool = False,
**kwargs,
):
"""Marshall chart's data into proto."""
import altair as alt
# Normally altair_chart.to_dict() would transform the dataframe used by the
# chart into an array of dictionaries. To avoid that, we install a
# transformer that replaces datasets with a reference by the object id of
# the dataframe. We then fill in the dataset manually later on.
datasets = {}
def id_transform(data):
"""Altair data transformer that returns a fake named dataset with the
object id."""
datasets[id(data)] = data
return {"name": str(id(data))}
alt.data_transformers.register("id", id_transform)
with alt.data_transformers.enable("id"):
chart_dict = altair_chart.to_dict()
# Put datasets back into the chart dict but note how they weren't
# transformed.
chart_dict["datasets"] = datasets
arrow_vega_lite.marshall(
vega_lite_chart,
chart_dict,
use_container_width=use_container_width,
**kwargs,
)

View File

@@ -0,0 +1,207 @@
# Copyright 2018-2022 Streamlit Inc.
#
# 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.
"""A Python wrapper around Vega-Lite."""
import json
from typing import Any, Dict, Optional, cast
import streamlit
import streamlit.elements.lib.dicttools as dicttools
from streamlit.logger import get_logger
from streamlit.proto.ArrowVegaLiteChart_pb2 import (
ArrowVegaLiteChart as ArrowVegaLiteChartProto,
)
from . import arrow
from .arrow import Data
LOGGER = get_logger(__name__)
class ArrowVegaLiteMixin:
def _arrow_vega_lite_chart(
self,
data: Data = None,
spec: Optional[Dict[str, Any]] = None,
use_container_width: bool = False,
**kwargs,
) -> "streamlit.delta_generator.DeltaGenerator":
"""Display a chart using the Vega-Lite library.
Parameters
----------
data : pandas.DataFrame, pandas.Styler, pyarrow.Table, numpy.ndarray, Iterable, dict, or None
Either the data to be plotted or a Vega-Lite spec containing the
data (which more closely follows the Vega-Lite API).
spec : dict or None
The Vega-Lite spec for the chart. If the spec was already passed in
the previous argument, this must be set to None. See
https://vega.github.io/vega-lite/docs/ for more info.
use_container_width : bool
If True, set the chart width to the column width. This takes
precedence over Vega-Lite's native `width` value.
**kwargs : any
Same as spec, but as keywords.
Example
-------
>>> import pandas as pd
>>> import numpy as np
>>>
>>> df = pd.DataFrame(
... np.random.randn(200, 3),
... columns=['a', 'b', 'c'])
>>>
>>> st._arrow_vega_lite_chart(df, {
... 'mark': {'type': 'circle', 'tooltip': True},
... 'encoding': {
... 'x': {'field': 'a', 'type': 'quantitative'},
... 'y': {'field': 'b', 'type': 'quantitative'},
... 'size': {'field': 'c', 'type': 'quantitative'},
... 'color': {'field': 'c', 'type': 'quantitative'},
... },
... })
Examples of Vega-Lite usage without Streamlit can be found at
https://vega.github.io/vega-lite/examples/. Most of those can be easily
translated to the syntax shown above.
"""
proto = ArrowVegaLiteChartProto()
marshall(
proto,
data,
spec,
use_container_width=use_container_width,
**kwargs,
)
return cast(
"streamlit.delta_generator.DeltaGenerator",
self.dg._enqueue("arrow_vega_lite_chart", proto),
)
@property
def dg(self) -> "streamlit.delta_generator.DeltaGenerator":
"""Get our DeltaGenerator."""
return cast("streamlit.delta_generator.DeltaGenerator", self)
def marshall(
proto: ArrowVegaLiteChartProto,
data: Data = None,
spec: Optional[Dict[str, Any]] = None,
use_container_width: bool = False,
**kwargs,
):
"""Construct a Vega-Lite chart object.
See DeltaGenerator.vega_lite_chart for docs.
"""
# Support passing data inside spec['datasets'] and spec['data'].
# (The data gets pulled out of the spec dict later on.)
if isinstance(data, dict) and spec is None:
spec = data
data = None
# Support passing no spec arg, but filling it with kwargs.
# Example:
# marshall(proto, baz='boz')
if spec is None:
spec = dict()
else:
# Clone the spec dict, since we may be mutating it.
spec = dict(spec)
# Support passing in kwargs. Example:
# marshall(proto, {foo: 'bar'}, baz='boz')
if len(kwargs):
# Merge spec with unflattened kwargs, where kwargs take precedence.
# This only works for string keys, but kwarg keys are strings anyways.
spec = dict(spec, **dicttools.unflatten(kwargs, _CHANNELS))
if len(spec) == 0:
raise ValueError("Vega-Lite charts require a non-empty spec dict.")
if "autosize" not in spec:
spec["autosize"] = {"type": "fit", "contains": "padding"}
# Pull data out of spec dict when it's in a 'datasets' key:
# marshall(proto, {datasets: {foo: df1, bar: df2}, ...})
if "datasets" in spec:
for k, v in spec["datasets"].items():
dataset = proto.datasets.add()
dataset.name = str(k)
dataset.has_name = True
arrow.marshall(dataset.data, v)
del spec["datasets"]
# Pull data out of spec dict when it's in a top-level 'data' key:
# marshall(proto, {data: df})
# marshall(proto, {data: {values: df, ...}})
# marshall(proto, {data: {url: 'url'}})
# marshall(proto, {data: {name: 'foo'}})
if "data" in spec:
data_spec = spec["data"]
if isinstance(data_spec, dict):
if "values" in data_spec:
data = data_spec["values"]
del spec["data"]
else:
data = data_spec
del spec["data"]
proto.spec = json.dumps(spec)
proto.use_container_width = use_container_width
if data is not None:
arrow.marshall(proto.data, data)
# See https://vega.github.io/vega-lite/docs/encoding.html
_CHANNELS = set(
[
"x",
"y",
"x2",
"y2",
"xError",
"yError2",
"xError",
"yError2",
"longitude",
"latitude",
"color",
"opacity",
"fillOpacity",
"strokeOpacity",
"strokeWidth",
"size",
"shape",
"text",
"tooltip",
"href",
"key",
"order",
"detail",
"facet",
"row",
"column",
]
)

View File

@@ -0,0 +1,39 @@
# Copyright 2018-2022 Streamlit Inc.
#
# 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 typing import cast
import streamlit
from streamlit.proto.Balloons_pb2 import Balloons as BalloonsProto
class BalloonsMixin:
def balloons(self):
"""Draw celebratory balloons.
Example
-------
>>> st.balloons()
...then watch your app and get ready for a celebration!
"""
balloons_proto = BalloonsProto()
balloons_proto.show = True
return self.dg._enqueue("balloons", balloons_proto)
@property
def dg(self) -> "streamlit.delta_generator.DeltaGenerator":
"""Get our DeltaGenerator."""
return cast("streamlit.delta_generator.DeltaGenerator", self)

View File

@@ -0,0 +1,104 @@
# Copyright 2018-2022 Streamlit Inc.
#
# 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.
"""A Python wrapper around Bokeh."""
import hashlib
import json
from typing import cast
import streamlit
from streamlit.errors import StreamlitAPIException
from streamlit.proto.BokehChart_pb2 import BokehChart as BokehChartProto
ST_BOKEH_VERSION = "2.4.1"
class BokehMixin:
def bokeh_chart(self, figure, use_container_width=False):
"""Display an interactive Bokeh chart.
Bokeh is a charting library for Python. The arguments to this function
closely follow the ones for Bokeh's `show` function. You can find
more about Bokeh at https://bokeh.pydata.org.
To show Bokeh charts in Streamlit, call `st.bokeh_chart`
wherever you would call Bokeh's `show`.
Parameters
----------
figure : bokeh.plotting.figure.Figure
A Bokeh figure to plot.
use_container_width : bool
If True, set the chart width to the column width. This takes
precedence over Bokeh's native `width` value.
Example
-------
>>> import streamlit as st
>>> from bokeh.plotting import figure
>>>
>>> x = [1, 2, 3, 4, 5]
>>> y = [6, 7, 2, 4, 5]
>>>
>>> p = figure(
... title='simple line example',
... x_axis_label='x',
... y_axis_label='y')
...
>>> p.line(x, y, legend_label='Trend', line_width=2)
>>>
>>> st.bokeh_chart(p, use_container_width=True)
.. output::
https://share.streamlit.io/streamlit/docs/main/python/api-examples-source/charts.bokeh_chart.py
height: 700px
"""
import bokeh
if bokeh.__version__ != ST_BOKEH_VERSION:
raise StreamlitAPIException(
f"Streamlit only supports Bokeh version {ST_BOKEH_VERSION}, "
f"but you have version {bokeh.__version__} installed. Please "
f"run `pip install --force-reinstall --no-deps bokeh=="
f"{ST_BOKEH_VERSION}` to install the correct version."
)
# Generate element ID from delta path
delta_path = self.dg._get_delta_path_str()
element_id = hashlib.md5(delta_path.encode()).hexdigest()
bokeh_chart_proto = BokehChartProto()
marshall(bokeh_chart_proto, figure, use_container_width, element_id)
return self.dg._enqueue("bokeh_chart", bokeh_chart_proto)
@property
def dg(self) -> "streamlit.delta_generator.DeltaGenerator":
"""Get our DeltaGenerator."""
return cast("streamlit.delta_generator.DeltaGenerator", self)
def marshall(proto, figure, use_container_width, element_id):
"""Construct a Bokeh chart object.
See DeltaGenerator.bokeh_chart for docs.
"""
from bokeh.embed import json_item
data = json_item(figure)
proto.figure = json.dumps(data)
proto.use_container_width = use_container_width
proto.element_id = element_id

View File

@@ -0,0 +1,386 @@
# Copyright 2018-2022 Streamlit Inc.
#
# 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.
import io
from streamlit.scriptrunner import ScriptRunContext, get_script_run_ctx
from streamlit.type_util import Key, to_key
from typing import cast, Optional, Union, BinaryIO, TextIO
from textwrap import dedent
import streamlit
from streamlit.errors import StreamlitAPIException
from streamlit.proto.Button_pb2 import Button as ButtonProto
from streamlit.in_memory_file_manager import in_memory_file_manager
from streamlit.proto.DownloadButton_pb2 import DownloadButton as DownloadButtonProto
from streamlit.state import (
register_widget,
WidgetArgs,
WidgetCallback,
WidgetKwargs,
)
from .form import current_form_id, is_in_form
from .utils import check_callback_rules, check_session_state_rules
FORM_DOCS_INFO = """
For more information, refer to the
[documentation for forms](https://docs.streamlit.io/library/api-reference/control-flow/st.form).
"""
DownloadButtonDataType = Union[str, bytes, TextIO, BinaryIO]
class ButtonMixin:
def button(
self,
label: str,
key: Optional[Key] = None,
help: Optional[str] = None,
on_click: Optional[WidgetCallback] = None,
args: Optional[WidgetArgs] = None,
kwargs: Optional[WidgetKwargs] = None,
*, # keyword-only arguments:
disabled: bool = False,
) -> bool:
"""Display a button widget.
Parameters
----------
label : str
A short label explaining to the user what this button is for.
key : str or int
An optional string or integer to use as the unique key for the widget.
If this is omitted, a key will be generated for the widget
based on its content. Multiple widgets of the same type may
not share the same key.
help : str
An optional tooltip that gets displayed when the button is
hovered over.
on_click : callable
An optional callback invoked when this button is clicked.
args : tuple
An optional tuple of args to pass to the callback.
kwargs : dict
An optional dict of kwargs to pass to the callback.
disabled : bool
An optional boolean, which disables the button if set to True. The
default is False. This argument can only be supplied by keyword.
Returns
-------
bool
True if the button was clicked on the last run of the app,
False otherwise.
Example
-------
>>> if st.button('Say hello'):
... st.write('Why hello there')
... else:
... st.write('Goodbye')
.. output::
https://share.streamlit.io/streamlit/docs/main/python/api-examples-source/widget.button.py
height: 220px
"""
key = to_key(key)
ctx = get_script_run_ctx()
return self.dg._button(
label,
key,
help,
is_form_submitter=False,
on_click=on_click,
args=args,
kwargs=kwargs,
disabled=disabled,
ctx=ctx,
)
def download_button(
self,
label: str,
data: DownloadButtonDataType,
file_name: Optional[str] = None,
mime: Optional[str] = None,
key: Optional[Key] = None,
help: Optional[str] = None,
on_click: Optional[WidgetCallback] = None,
args: Optional[WidgetArgs] = None,
kwargs: Optional[WidgetKwargs] = None,
*, # keyword-only arguments:
disabled: bool = False,
) -> bool:
"""Display a download button widget.
This is useful when you would like to provide a way for your users
to download a file directly from your app.
Note that the data to be downloaded is stored in-memory while the
user is connected, so it's a good idea to keep file sizes under a
couple hundred megabytes to conserve memory.
Parameters
----------
label : str
A short label explaining to the user what this button is for.
data : str or bytes or file
The contents of the file to be downloaded. See example below for
caching techniques to avoid recomputing this data unnecessarily.
file_name: str
An optional string to use as the name of the file to be downloaded,
such as 'my_file.csv'. If not specified, the name will be
automatically generated.
mime : str or None
The MIME type of the data. If None, defaults to "text/plain"
(if data is of type *str* or is a textual *file*) or
"application/octet-stream" (if data is of type *bytes* or is a
binary *file*).
key : str or int
An optional string or integer to use as the unique key for the widget.
If this is omitted, a key will be generated for the widget
based on its content. Multiple widgets of the same type may
not share the same key.
help : str
An optional tooltip that gets displayed when the button is
hovered over.
on_click : callable
An optional callback invoked when this button is clicked.
args : tuple
An optional tuple of args to pass to the callback.
kwargs : dict
An optional dict of kwargs to pass to the callback.
disabled : bool
An optional boolean, which disables the download button if set to
True. The default is False. This argument can only be supplied by
keyword.
Returns
-------
bool
True if the button was clicked on the last run of the app,
False otherwise.
Examples
--------
Download a large DataFrame as a CSV:
>>> @st.cache
... def convert_df(df):
... # IMPORTANT: Cache the conversion to prevent computation on every rerun
... return df.to_csv().encode('utf-8')
>>>
>>> csv = convert_df(my_large_df)
>>>
>>> st.download_button(
... label="Download data as CSV",
... data=csv,
... file_name='large_df.csv',
... mime='text/csv',
... )
Download a string as a file:
>>> text_contents = '''This is some text'''
>>> st.download_button('Download some text', text_contents)
Download a binary file:
>>> binary_contents = b'example content'
>>> # Defaults to 'application/octet-stream'
>>> st.download_button('Download binary file', binary_contents)
Download an image:
>>> with open("flower.png", "rb") as file:
... btn = st.download_button(
... label="Download image",
... data=file,
... file_name="flower.png",
... mime="image/png"
... )
.. output::
https://share.streamlit.io/streamlit/docs/main/python/api-examples-source/widget.download_button.py
height: 335px
"""
ctx = get_script_run_ctx()
return self._download_button(
label=label,
data=data,
file_name=file_name,
mime=mime,
key=key,
help=help,
on_click=on_click,
args=args,
kwargs=kwargs,
disabled=disabled,
ctx=ctx,
)
def _download_button(
self,
label: str,
data: DownloadButtonDataType,
file_name: Optional[str] = None,
mime: Optional[str] = None,
key: Optional[Key] = None,
help: Optional[str] = None,
on_click: Optional[WidgetCallback] = None,
args: Optional[WidgetArgs] = None,
kwargs: Optional[WidgetKwargs] = None,
*, # keyword-only arguments:
disabled: bool = False,
ctx: Optional[ScriptRunContext] = None,
) -> bool:
key = to_key(key)
check_session_state_rules(default_value=None, key=key, writes_allowed=False)
if is_in_form(self.dg):
raise StreamlitAPIException(
f"`st.download_button()` can't be used in an `st.form()`.{FORM_DOCS_INFO}"
)
download_button_proto = DownloadButtonProto()
download_button_proto.label = label
download_button_proto.default = False
marshall_file(
self.dg._get_delta_path_str(), data, download_button_proto, mime, file_name
)
if help is not None:
download_button_proto.help = dedent(help)
def deserialize_button(ui_value, widget_id=""):
return ui_value or False
current_value, _ = register_widget(
"download_button",
download_button_proto,
user_key=key,
on_change_handler=on_click,
args=args,
kwargs=kwargs,
deserializer=deserialize_button,
serializer=bool,
ctx=ctx,
)
# This needs to be done after register_widget because we don't want
# the following proto fields to affect a widget's ID.
download_button_proto.disabled = disabled
self.dg._enqueue("download_button", download_button_proto)
return cast(bool, current_value)
def _button(
self,
label: str,
key: Optional[str],
help: Optional[str],
is_form_submitter: bool,
on_click: Optional[WidgetCallback] = None,
args: Optional[WidgetArgs] = None,
kwargs: Optional[WidgetKwargs] = None,
*, # keyword-only arguments:
disabled: bool = False,
ctx: Optional[ScriptRunContext] = None,
) -> bool:
if not is_form_submitter:
check_callback_rules(self.dg, on_click)
check_session_state_rules(default_value=None, key=key, writes_allowed=False)
# It doesn't make sense to create a button inside a form (except
# for the "Form Submitter" button that's automatically created in
# every form). We throw an error to warn the user about this.
# We omit this check for scripts running outside streamlit, because
# they will have no script_run_ctx.
if streamlit._is_running_with_streamlit:
if is_in_form(self.dg) and not is_form_submitter:
raise StreamlitAPIException(
f"`st.button()` can't be used in an `st.form()`.{FORM_DOCS_INFO}"
)
elif not is_in_form(self.dg) and is_form_submitter:
raise StreamlitAPIException(
f"`st.form_submit_button()` must be used inside an `st.form()`.{FORM_DOCS_INFO}"
)
button_proto = ButtonProto()
button_proto.label = label
button_proto.default = False
button_proto.is_form_submitter = is_form_submitter
button_proto.form_id = current_form_id(self.dg)
if help is not None:
button_proto.help = dedent(help)
def deserialize_button(ui_value: bool, widget_id: str = "") -> bool:
return ui_value or False
current_value, _ = register_widget(
"button",
button_proto,
user_key=key,
on_change_handler=on_click,
args=args,
kwargs=kwargs,
deserializer=deserialize_button,
serializer=bool,
ctx=ctx,
)
# This needs to be done after register_widget because we don't want
# the following proto fields to affect a widget's ID.
button_proto.disabled = disabled
self.dg._enqueue("button", button_proto)
return cast(bool, current_value)
@property
def dg(self) -> "streamlit.delta_generator.DeltaGenerator":
"""Get our DeltaGenerator."""
return cast("streamlit.delta_generator.DeltaGenerator", self)
def marshall_file(coordinates, data, proto_download_button, mimetype, file_name=None):
if isinstance(data, str):
data = data.encode()
mimetype = mimetype or "text/plain"
elif isinstance(data, io.TextIOWrapper):
string_data = data.read()
data = string_data.encode()
mimetype = mimetype or "text/plain"
# Assume bytes; try methods until we run out.
elif isinstance(data, bytes):
mimetype = mimetype or "application/octet-stream"
elif isinstance(data, io.BytesIO):
data.seek(0)
data = data.getvalue()
mimetype = mimetype or "application/octet-stream"
elif isinstance(data, io.RawIOBase) or isinstance(data, io.BufferedReader):
data.seek(0)
data = data.read()
mimetype = mimetype or "application/octet-stream"
else:
raise RuntimeError("Invalid binary data format: %s" % type(data))
this_file = in_memory_file_manager.add(
data, mimetype, coordinates, file_name=file_name, is_for_static_download=True
)
proto_download_button.url = this_file.url

View File

@@ -0,0 +1,236 @@
# Copyright 2018-2022 Streamlit Inc.
#
# 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 streamlit.type_util import Key, to_key
from textwrap import dedent
from typing import Optional, cast, List
import streamlit
from streamlit.proto.CameraInput_pb2 import (
CameraInput as CameraInputProto,
)
from streamlit.scriptrunner import ScriptRunContext, get_script_run_ctx
from streamlit.state import (
register_widget,
WidgetArgs,
WidgetCallback,
WidgetKwargs,
)
from ..proto.Common_pb2 import (
FileUploaderState as FileUploaderStateProto,
UploadedFileInfo as UploadedFileInfoProto,
)
from ..uploaded_file_manager import UploadedFile, UploadedFileRec
from .form import current_form_id
from .utils import check_callback_rules, check_session_state_rules
SomeUploadedSnapshotFile = Optional[UploadedFile]
class CameraInputMixin:
def camera_input(
self,
label: str,
key: Optional[Key] = None,
help: Optional[str] = None,
on_change: Optional[WidgetCallback] = None,
args: Optional[WidgetArgs] = None,
kwargs: Optional[WidgetKwargs] = None,
*, # keyword-only arguments:
disabled: bool = False,
) -> SomeUploadedSnapshotFile:
"""Display a widget that returns pictures from the user's webcam.
Parameters
----------
label : str
A short label explaining to the user what this widget is used for.
key : str or int
An optional string or integer to use as the unique key for the widget.
If this is omitted, a key will be generated for the widget
based on its content. Multiple widgets of the same type may
not share the same key.
help : str
A tooltip that gets displayed next to the camera input.
on_change : callable
An optional callback invoked when this camera_input's value
changes.
args : tuple
An optional tuple of args to pass to the callback.
kwargs : dict
An optional dict of kwargs to pass to the callback.
disabled : bool
An optional boolean, which disables the camera input if set to
True. The default is False. This argument can only be supplied by
keyword.
Returns
-------
None or UploadedFile
The UploadedFile class is a subclass of BytesIO, and therefore
it is "file-like". This means you can pass them anywhere where
a file is expected.
Examples
--------
>>> import streamlit as st
>>>
>>> picture = st.camera_input("Take a picture")
>>>
>>> if picture:
... st.image(picture)
"""
ctx = get_script_run_ctx()
return self._camera_input(
label=label,
key=key,
help=help,
on_change=on_change,
args=args,
kwargs=kwargs,
disabled=disabled,
ctx=ctx,
)
def _camera_input(
self,
label: str,
key: Optional[Key] = None,
help: Optional[str] = None,
on_change: Optional[WidgetCallback] = None,
args: Optional[WidgetArgs] = None,
kwargs: Optional[WidgetKwargs] = None,
*, # keyword-only arguments:
disabled: bool = False,
ctx: Optional[ScriptRunContext] = None,
) -> SomeUploadedSnapshotFile:
key = to_key(key)
check_callback_rules(self.dg, on_change)
check_session_state_rules(default_value=None, key=key, writes_allowed=False)
camera_input_proto = CameraInputProto()
camera_input_proto.label = label
camera_input_proto.form_id = current_form_id(self.dg)
if help is not None:
camera_input_proto.help = dedent(help)
def serialize_camera_image_input(
snapshot: SomeUploadedSnapshotFile,
) -> FileUploaderStateProto:
state_proto = FileUploaderStateProto()
ctx = get_script_run_ctx()
if ctx is None:
return state_proto
# ctx.uploaded_file_mgr._file_id_counter stores the id to use for
# the *next* uploaded file, so the current highest file id is the
# counter minus 1.
state_proto.max_file_id = ctx.uploaded_file_mgr._file_id_counter - 1
if not snapshot:
return state_proto
file_info: UploadedFileInfoProto = state_proto.uploaded_file_info.add()
file_info.id = snapshot.id
file_info.name = snapshot.name
file_info.size = snapshot.size
return state_proto
def deserialize_camera_image_input(
ui_value: Optional[FileUploaderStateProto], widget_id: str
) -> SomeUploadedSnapshotFile:
file_recs = self._get_file_recs_for_camera_input_widget(widget_id, ui_value)
if len(file_recs) == 0:
return_value = None
else:
return_value = UploadedFile(file_recs[0])
return return_value
widget_value, _ = register_widget(
"camera_input",
camera_input_proto,
user_key=key,
on_change_handler=on_change,
args=args,
kwargs=kwargs,
deserializer=deserialize_camera_image_input,
serializer=serialize_camera_image_input,
ctx=ctx,
)
# This needs to be done after register_widget because we don't want
# the following proto fields to affect a widget's ID.
camera_input_proto.disabled = disabled
ctx = get_script_run_ctx()
camera_image_input_state = serialize_camera_image_input(widget_value)
uploaded_shapshot_info = camera_image_input_state.uploaded_file_info
if ctx is not None and len(uploaded_shapshot_info) != 0:
newest_file_id = camera_image_input_state.max_file_id
active_file_ids = [f.id for f in uploaded_shapshot_info]
ctx.uploaded_file_mgr.remove_orphaned_files(
session_id=ctx.session_id,
widget_id=camera_input_proto.id,
newest_file_id=newest_file_id,
active_file_ids=active_file_ids,
)
self.dg._enqueue("camera_input", camera_input_proto)
return cast(SomeUploadedSnapshotFile, widget_value)
@property
def dg(self) -> "streamlit.delta_generator.DeltaGenerator":
"""Get our DeltaGenerator."""
return cast("streamlit.delta_generator.DeltaGenerator", self)
@staticmethod
def _get_file_recs_for_camera_input_widget(
widget_id: str, widget_value: Optional[FileUploaderStateProto]
) -> List[UploadedFileRec]:
if widget_value is None:
return []
ctx = get_script_run_ctx()
if ctx is None:
return []
uploaded_file_info = widget_value.uploaded_file_info
if len(uploaded_file_info) == 0:
return []
active_file_ids = [f.id for f in uploaded_file_info]
# Grab the files that correspond to our active file IDs.
return ctx.uploaded_file_mgr.get_files(
session_id=ctx.session_id,
widget_id=widget_id,
file_ids=active_file_ids,
)

View File

@@ -0,0 +1,155 @@
# Copyright 2018-2022 Streamlit Inc.
#
# 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 streamlit.scriptrunner import ScriptRunContext, get_script_run_ctx
from streamlit.type_util import Key, to_key
from textwrap import dedent
from typing import cast, Optional
import streamlit
from streamlit.proto.Checkbox_pb2 import Checkbox as CheckboxProto
from streamlit.state import (
register_widget,
WidgetArgs,
WidgetCallback,
WidgetKwargs,
)
from .form import current_form_id
from .utils import check_callback_rules, check_session_state_rules
class CheckboxMixin:
def checkbox(
self,
label: str,
value: bool = False,
key: Optional[Key] = None,
help: Optional[str] = None,
on_change: Optional[WidgetCallback] = None,
args: Optional[WidgetArgs] = None,
kwargs: Optional[WidgetKwargs] = None,
*, # keyword-only arguments:
disabled: bool = False,
) -> bool:
"""Display a checkbox widget.
Parameters
----------
label : str
A short label explaining to the user what this checkbox is for.
value : bool
Preselect the checkbox when it first renders. This will be
cast to bool internally.
key : str or int
An optional string or integer to use as the unique key for the widget.
If this is omitted, a key will be generated for the widget
based on its content. Multiple widgets of the same type may
not share the same key.
help : str
An optional tooltip that gets displayed next to the checkbox.
on_change : callable
An optional callback invoked when this checkbox's value changes.
args : tuple
An optional tuple of args to pass to the callback.
kwargs : dict
An optional dict of kwargs to pass to the callback.
disabled : bool
An optional boolean, which disables the checkbox if set to True.
The default is False. This argument can only be supplied by keyword.
Returns
-------
bool
Whether or not the checkbox is checked.
Example
-------
>>> agree = st.checkbox('I agree')
>>>
>>> if agree:
... st.write('Great!')
.. output::
https://share.streamlit.io/streamlit/docs/main/python/api-examples-source/widget.checkbox.py
height: 220px
"""
ctx = get_script_run_ctx()
return self._checkbox(
label=label,
value=value,
key=key,
help=help,
on_change=on_change,
args=args,
kwargs=kwargs,
disabled=disabled,
ctx=ctx,
)
def _checkbox(
self,
label: str,
value: bool = False,
key: Optional[Key] = None,
help: Optional[str] = None,
on_change: Optional[WidgetCallback] = None,
args: Optional[WidgetArgs] = None,
kwargs: Optional[WidgetKwargs] = None,
*, # keyword-only arguments:
disabled: bool = False,
ctx: Optional[ScriptRunContext] = None,
) -> bool:
key = to_key(key)
check_callback_rules(self.dg, on_change)
check_session_state_rules(
default_value=None if value is False else value, key=key
)
checkbox_proto = CheckboxProto()
checkbox_proto.label = label
checkbox_proto.default = bool(value)
checkbox_proto.form_id = current_form_id(self.dg)
if help is not None:
checkbox_proto.help = dedent(help)
def deserialize_checkbox(ui_value: Optional[bool], widget_id: str = "") -> bool:
return bool(ui_value if ui_value is not None else value)
current_value, set_frontend_value = register_widget(
"checkbox",
checkbox_proto,
user_key=key,
on_change_handler=on_change,
args=args,
kwargs=kwargs,
deserializer=deserialize_checkbox,
serializer=bool,
ctx=ctx,
)
# This needs to be done after register_widget because we don't want
# the following proto fields to affect a widget's ID.
checkbox_proto.disabled = disabled
if set_frontend_value:
checkbox_proto.value = current_value
checkbox_proto.set_value = True
self.dg._enqueue("checkbox", checkbox_proto)
return cast(bool, current_value)
@property
def dg(self) -> "streamlit.delta_generator.DeltaGenerator":
"""Get our DeltaGenerator."""
return cast("streamlit.delta_generator.DeltaGenerator", self)

View File

@@ -0,0 +1,183 @@
# Copyright 2018-2022 Streamlit Inc.
#
# 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.
import re
from streamlit.scriptrunner import ScriptRunContext, get_script_run_ctx
from streamlit.type_util import Key, to_key
from textwrap import dedent
from typing import Optional, cast
import streamlit
from streamlit.errors import StreamlitAPIException
from streamlit.proto.ColorPicker_pb2 import ColorPicker as ColorPickerProto
from streamlit.state import register_widget
from streamlit.state import (
WidgetArgs,
WidgetCallback,
WidgetKwargs,
)
from .form import current_form_id
from .utils import check_callback_rules, check_session_state_rules
class ColorPickerMixin:
def color_picker(
self,
label: str,
value: Optional[str] = None,
key: Optional[Key] = None,
help: Optional[str] = None,
on_change: Optional[WidgetCallback] = None,
args: Optional[WidgetArgs] = None,
kwargs: Optional[WidgetKwargs] = None,
*, # keyword-only arguments:
disabled: bool = False,
) -> str:
"""Display a color picker widget.
Parameters
----------
label : str
A short label explaining to the user what this input is for.
value : str
The hex value of this widget when it first renders. If None,
defaults to black.
key : str or int
An optional string or integer to use as the unique key for the widget.
If this is omitted, a key will be generated for the widget
based on its content. Multiple widgets of the same type may
not share the same key.
help : str
An optional tooltip that gets displayed next to the color picker.
on_change : callable
An optional callback invoked when this color_picker's value
changes.
args : tuple
An optional tuple of args to pass to the callback.
kwargs : dict
An optional dict of kwargs to pass to the callback.
disabled : bool
An optional boolean, which disables the color picker if set to
True. The default is False. This argument can only be supplied by
keyword.
Returns
-------
str
The selected color as a hex string.
Example
-------
>>> color = st.color_picker('Pick A Color', '#00f900')
>>> st.write('The current color is', color)
.. output::
https://share.streamlit.io/streamlit/docs/main/python/api-examples-source/widget.color_picker.py
height: 335px
"""
ctx = get_script_run_ctx()
return self._color_picker(
label=label,
value=value,
key=key,
help=help,
on_change=on_change,
args=args,
kwargs=kwargs,
disabled=disabled,
ctx=ctx,
)
def _color_picker(
self,
label: str,
value: Optional[str] = None,
key: Optional[Key] = None,
help: Optional[str] = None,
on_change: Optional[WidgetCallback] = None,
args: Optional[WidgetArgs] = None,
kwargs: Optional[WidgetKwargs] = None,
*, # keyword-only arguments:
disabled: bool = False,
ctx: Optional[ScriptRunContext] = None,
) -> str:
key = to_key(key)
check_callback_rules(self.dg, on_change)
check_session_state_rules(default_value=value, key=key)
# set value default
if value is None:
value = "#000000"
# make sure the value is a string
if not isinstance(value, str):
raise StreamlitAPIException(
"""
Color Picker Value has invalid type: %s. Expects a hex string
like '#00FFAA' or '#000'.
"""
% type(value).__name__
)
# validate the value and expects a hex string
match = re.match(r"^#(?:[0-9a-fA-F]{3}){1,2}$", value)
if not match:
raise StreamlitAPIException(
"""
'%s' is not a valid hex code for colors. Valid ones are like
'#00FFAA' or '#000'.
"""
% value
)
color_picker_proto = ColorPickerProto()
color_picker_proto.label = label
color_picker_proto.default = str(value)
color_picker_proto.form_id = current_form_id(self.dg)
if help is not None:
color_picker_proto.help = dedent(help)
def deserialize_color_picker(
ui_value: Optional[str], widget_id: str = ""
) -> str:
return str(ui_value if ui_value is not None else value)
current_value, set_frontend_value = register_widget(
"color_picker",
color_picker_proto,
user_key=key,
on_change_handler=on_change,
args=args,
kwargs=kwargs,
deserializer=deserialize_color_picker,
serializer=str,
ctx=ctx,
)
# This needs to be done after register_widget because we don't want
# the following proto fields to affect a widget's ID.
color_picker_proto.disabled = disabled
if set_frontend_value:
color_picker_proto.value = current_value
color_picker_proto.set_value = True
self.dg._enqueue("color_picker", color_picker_proto)
return cast(str, current_value)
@property
def dg(self) -> "streamlit.delta_generator.DeltaGenerator":
"""Get our DeltaGenerator."""
return cast("streamlit.delta_generator.DeltaGenerator", self)

View File

@@ -0,0 +1,441 @@
# Copyright 2018-2022 Streamlit Inc.
#
# 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.
"""Selects between our two DataFrame serialization methods ("legacy" and
"arrow") based on a config option"""
from typing import cast
import streamlit
from streamlit import config
def _use_arrow() -> bool:
"""True if we're using Apache Arrow for DataFrame serialization."""
# Explicitly coerce to bool here because mypy is (incorrectly) complaining
# that we're trying to return 'Any'.
return bool(config.get_option("global.dataFrameSerialization") == "arrow")
class DataFrameSelectorMixin:
def dataframe(self, data=None, width=None, height=None):
"""Display a dataframe as an interactive table.
Parameters
----------
data : pandas.DataFrame, pandas.Styler, pyarrow.Table, numpy.ndarray, Iterable, dict, or None
The data to display.
If 'data' is a pandas.Styler, it will be used to style its
underyling DataFrame. Streamlit supports custom cell
values and colors. (It does not support some of the more exotic
pandas styling features, like bar charts, hovering, and captions.)
Styler support is experimental!
Pyarrow tables are not supported by Streamlit's legacy DataFrame serialization
(i.e. with `config.dataFrameSerialization = "legacy"`).
To use pyarrow tables, please enable pyarrow by changing the config setting,
`config.dataFrameSerialization = "arrow"`.
width : int or None
Desired width of the UI element expressed in pixels. If None, a
default width based on the page width is used.
height : int or None
Desired height of the UI element expressed in pixels. If None, a
default height is used.
Examples
--------
>>> df = pd.DataFrame(
... np.random.randn(50, 20),
... columns=('col %d' % i for i in range(20)))
...
>>> st.dataframe(df) # Same as st.write(df)
.. output::
https://share.streamlit.io/streamlit/docs/main/python/api-examples-source/data.dataframe.py
height: 410px
>>> st.dataframe(df, 200, 100)
You can also pass a Pandas Styler object to change the style of
the rendered DataFrame:
>>> df = pd.DataFrame(
... np.random.randn(10, 20),
... columns=('col %d' % i for i in range(20)))
...
>>> st.dataframe(df.style.highlight_max(axis=0))
.. output::
https://share.streamlit.io/streamlit/docs/main/python/api-examples-source/data.dataframe1.py
height: 410px
"""
if _use_arrow():
return self.dg._arrow_dataframe(data, width, height)
else:
return self.dg._legacy_dataframe(data, width, height)
def table(self, data=None):
"""Display a static table.
This differs from `st.dataframe` in that the table in this case is
static: its entire contents are laid out directly on the page.
Parameters
----------
data : pandas.DataFrame, pandas.Styler, pyarrow.Table, numpy.ndarray, Iterable, dict, or None
The table data.
Pyarrow tables are not supported by Streamlit's legacy DataFrame serialization
(i.e. with `config.dataFrameSerialization = "legacy"`).
To use pyarrow tables, please enable pyarrow by changing the config setting,
`config.dataFrameSerialization = "arrow"`.
Example
-------
>>> df = pd.DataFrame(
... np.random.randn(10, 5),
... columns=('col %d' % i for i in range(5)))
...
>>> st.table(df)
.. output::
https://share.streamlit.io/streamlit/docs/main/python/api-examples-source/data.table.py
height: 480px
"""
if _use_arrow():
return self.dg._arrow_table(data)
else:
return self.dg._legacy_table(data)
def line_chart(self, data=None, width=0, height=0, use_container_width=True):
"""Display a line chart.
This is syntax-sugar around st.altair_chart. The main difference
is this command uses the data's own column and indices to figure out
the chart's spec. As a result this is easier to use for many "just plot
this" scenarios, while being less customizable.
If st.line_chart does not guess the data specification
correctly, try specifying your desired chart using st.altair_chart.
Parameters
----------
data : pandas.DataFrame, pandas.Styler, pyarrow.Table, numpy.ndarray, Iterable, dict or None
Data to be plotted.
Pyarrow tables are not supported by Streamlit's legacy DataFrame serialization
(i.e. with `config.dataFrameSerialization = "legacy"`).
To use pyarrow tables, please enable pyarrow by changing the config setting,
`config.dataFrameSerialization = "arrow"`.
width : int
The chart width in pixels. If 0, selects the width automatically.
height : int
The chart height in pixels. If 0, selects the height automatically.
use_container_width : bool
If True, set the chart width to the column width. This takes
precedence over the width argument.
Example
-------
>>> chart_data = pd.DataFrame(
... np.random.randn(20, 3),
... columns=['a', 'b', 'c'])
...
>>> st.line_chart(chart_data)
.. output::
https://share.streamlit.io/streamlit/docs/main/python/api-examples-source/charts.line_chart.py
height: 400px
"""
if _use_arrow():
return self.dg._arrow_line_chart(data, width, height, use_container_width)
else:
return self.dg._legacy_line_chart(data, width, height, use_container_width)
def area_chart(self, data=None, width=0, height=0, use_container_width=True):
"""Display an area chart.
This is just syntax-sugar around st.altair_chart. The main difference
is this command uses the data's own column and indices to figure out
the chart's spec. As a result this is easier to use for many "just plot
this" scenarios, while being less customizable.
If st.area_chart does not guess the data specification
correctly, try specifying your desired chart using st.altair_chart.
Parameters
----------
data : pandas.DataFrame, pandas.Styler, pyarrow.Table, numpy.ndarray, Iterable, or dict
Data to be plotted.
Pyarrow tables are not supported by Streamlit's legacy DataFrame serialization
(i.e. with `config.dataFrameSerialization = "legacy"`).
To use pyarrow tables, please enable pyarrow by changing the config setting,
`config.dataFrameSerialization = "arrow"`.
width : int
The chart width in pixels. If 0, selects the width automatically.
height : int
The chart height in pixels. If 0, selects the height automatically.
use_container_width : bool
If True, set the chart width to the column width. This takes
precedence over the width argument.
Example
-------
>>> chart_data = pd.DataFrame(
... np.random.randn(20, 3),
... columns=['a', 'b', 'c'])
...
>>> st.area_chart(chart_data)
.. output::
https://share.streamlit.io/streamlit/docs/main/python/api-examples-source/charts.area_chart.py
height: 400px
"""
if _use_arrow():
return self.dg._arrow_area_chart(data, width, height, use_container_width)
else:
return self.dg._legacy_area_chart(data, width, height, use_container_width)
def bar_chart(self, data=None, width=0, height=0, use_container_width=True):
"""Display a bar chart.
This is just syntax-sugar around st.altair_chart. The main difference
is this command uses the data's own column and indices to figure out
the chart's spec. As a result this is easier to use for many "just plot
this" scenarios, while being less customizable.
If st.bar_chart does not guess the data specification
correctly, try specifying your desired chart using st.altair_chart.
Parameters
----------
data : pandas.DataFrame, pandas.Styler, pyarrow.Table, numpy.ndarray, Iterable, or dict
Data to be plotted.
Pyarrow tables are not supported by Streamlit's legacy DataFrame serialization
(i.e. with `config.dataFrameSerialization = "legacy"`).
To use pyarrow tables, please enable pyarrow by changing the config setting,
`config.dataFrameSerialization = "arrow"`.
width : int
The chart width in pixels. If 0, selects the width automatically.
height : int
The chart height in pixels. If 0, selects the height automatically.
use_container_width : bool
If True, set the chart width to the column width. This takes
precedence over the width argument.
Example
-------
>>> chart_data = pd.DataFrame(
... np.random.randn(50, 3),
... columns=["a", "b", "c"])
...
>>> st.bar_chart(chart_data)
.. output::
https://share.streamlit.io/streamlit/docs/main/python/api-examples-source/charts.bar_chart.py
height: 400px
"""
if _use_arrow():
return self.dg._arrow_bar_chart(data, width, height, use_container_width)
else:
return self.dg._legacy_bar_chart(data, width, height, use_container_width)
def altair_chart(self, altair_chart, use_container_width=False):
"""Display a chart using the Altair library.
Parameters
----------
altair_chart : altair.vegalite.v2.api.Chart
The Altair chart object to display.
use_container_width : bool
If True, set the chart width to the column width. This takes
precedence over Altair's native `width` value.
Example
-------
>>> import pandas as pd
>>> import numpy as np
>>> import altair as alt
>>>
>>> df = pd.DataFrame(
... np.random.randn(200, 3),
... columns=['a', 'b', 'c'])
...
>>> c = alt.Chart(df).mark_circle().encode(
... x='a', y='b', size='c', color='c', tooltip=['a', 'b', 'c'])
>>>
>>> st.altair_chart(c, use_container_width=True)
Examples of Altair charts can be found at
https://altair-viz.github.io/gallery/.
.. output::
https://share.streamlit.io/streamlit/docs/main/python/api-examples-source/charts.vega_lite_chart.py
height: 300px
"""
if _use_arrow():
return self.dg._arrow_altair_chart(altair_chart, use_container_width)
else:
return self.dg._legacy_altair_chart(altair_chart, use_container_width)
def vega_lite_chart(
self,
data=None,
spec=None,
use_container_width=False,
**kwargs,
):
"""Display a chart using the Vega-Lite library.
Parameters
----------
data : pandas.DataFrame, pandas.Styler, pyarrow.Table, numpy.ndarray, Iterable, dict, or None
Either the data to be plotted or a Vega-Lite spec containing the
data (which more closely follows the Vega-Lite API).
Pyarrow tables are not supported by Streamlit's legacy DataFrame serialization
(i.e. with `config.dataFrameSerialization = "legacy"`).
To use pyarrow tables, please enable pyarrow by changing the config setting,
`config.dataFrameSerialization = "arrow"`.
spec : dict or None
The Vega-Lite spec for the chart. If the spec was already passed in
the previous argument, this must be set to None. See
https://vega.github.io/vega-lite/docs/ for more info.
use_container_width : bool
If True, set the chart width to the column width. This takes
precedence over Vega-Lite's native `width` value.
**kwargs : any
Same as spec, but as keywords.
Example
-------
>>> import pandas as pd
>>> import numpy as np
>>>
>>> df = pd.DataFrame(
... np.random.randn(200, 3),
... columns=['a', 'b', 'c'])
>>>
>>> st.vega_lite_chart(df, {
... 'mark': {'type': 'circle', 'tooltip': True},
... 'encoding': {
... 'x': {'field': 'a', 'type': 'quantitative'},
... 'y': {'field': 'b', 'type': 'quantitative'},
... 'size': {'field': 'c', 'type': 'quantitative'},
... 'color': {'field': 'c', 'type': 'quantitative'},
... },
... })
.. output::
https://share.streamlit.io/streamlit/docs/main/python/api-examples-source/charts.vega_lite_chart.py
height: 300px
Examples of Vega-Lite usage without Streamlit can be found at
https://vega.github.io/vega-lite/examples/. Most of those can be easily
translated to the syntax shown above.
"""
if _use_arrow():
return self.dg._arrow_vega_lite_chart(
data, spec, use_container_width, **kwargs
)
else:
return self.dg._legacy_vega_lite_chart(
data, spec, use_container_width, **kwargs
)
def add_rows(self, data=None, **kwargs):
"""Concatenate a dataframe to the bottom of the current one.
Parameters
----------
data : pandas.DataFrame, pandas.Styler, pyarrow.Table, numpy.ndarray, Iterable, dict, or None
Table to concat. Optional.
Pyarrow tables are not supported by Streamlit's legacy DataFrame serialization
(i.e. with `config.dataFrameSerialization = "legacy"`).
To use pyarrow tables, please enable pyarrow by changing the config setting,
`config.dataFrameSerialization = "arrow"`.
**kwargs : pandas.DataFrame, numpy.ndarray, Iterable, dict, or None
The named dataset to concat. Optional. You can only pass in 1
dataset (including the one in the data parameter).
Example
-------
>>> df1 = pd.DataFrame(
... np.random.randn(50, 20),
... columns=('col %d' % i for i in range(20)))
...
>>> my_table = st.table(df1)
>>>
>>> df2 = pd.DataFrame(
... np.random.randn(50, 20),
... columns=('col %d' % i for i in range(20)))
...
>>> my_table.add_rows(df2)
>>> # Now the table shown in the Streamlit app contains the data for
>>> # df1 followed by the data for df2.
You can do the same thing with plots. For example, if you want to add
more data to a line chart:
>>> # Assuming df1 and df2 from the example above still exist...
>>> my_chart = st.line_chart(df1)
>>> my_chart.add_rows(df2)
>>> # Now the chart shown in the Streamlit app contains the data for
>>> # df1 followed by the data for df2.
And for plots whose datasets are named, you can pass the data with a
keyword argument where the key is the name:
>>> my_chart = st.vega_lite_chart({
... 'mark': 'line',
... 'encoding': {'x': 'a', 'y': 'b'},
... 'datasets': {
... 'some_fancy_name': df1, # <-- named dataset
... },
... 'data': {'name': 'some_fancy_name'},
... }),
>>> my_chart.add_rows(some_fancy_name=df2) # <-- name used as keyword
"""
if _use_arrow():
return self.dg._arrow_add_rows(data, **kwargs)
else:
return self.dg._legacy_add_rows(data, **kwargs)
@property
def dg(self) -> "streamlit.delta_generator.DeltaGenerator":
"""Get our DeltaGenerator."""
return cast("streamlit.delta_generator.DeltaGenerator", self)

View File

@@ -0,0 +1,117 @@
# Copyright 2018-2022 Streamlit Inc.
#
# 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 typing import cast, Any, Dict
import streamlit
import json
from streamlit.proto.DeckGlJsonChart_pb2 import DeckGlJsonChart as PydeckProto
class PydeckMixin:
def pydeck_chart(self, pydeck_obj=None, use_container_width=False):
"""Draw a chart using the PyDeck library.
This supports 3D maps, point clouds, and more! More info about PyDeck
at https://deckgl.readthedocs.io/en/latest/.
These docs are also quite useful:
- DeckGL docs: https://github.com/uber/deck.gl/tree/master/docs
- DeckGL JSON docs: https://github.com/uber/deck.gl/tree/master/modules/json
When using this command, we advise all users to use a personal Mapbox
token. This ensures the map tiles used in this chart are more
robust. You can do this with the mapbox.token config option.
To get a token for yourself, create an account at
https://mapbox.com. It's free! (for moderate usage levels). For more info
on how to set config options, see
https://docs.streamlit.io/library/advanced-features/configuration#set-configuration-options
Parameters
----------
spec: pydeck.Deck or None
Object specifying the PyDeck chart to draw.
Example
-------
Here's a chart using a HexagonLayer and a ScatterplotLayer on top of
the light map style:
>>> df = pd.DataFrame(
... np.random.randn(1000, 2) / [50, 50] + [37.76, -122.4],
... columns=['lat', 'lon'])
>>>
>>> st.pydeck_chart(pdk.Deck(
... map_style='mapbox://styles/mapbox/light-v9',
... initial_view_state=pdk.ViewState(
... latitude=37.76,
... longitude=-122.4,
... zoom=11,
... pitch=50,
... ),
... layers=[
... pdk.Layer(
... 'HexagonLayer',
... data=df,
... get_position='[lon, lat]',
... radius=200,
... elevation_scale=4,
... elevation_range=[0, 1000],
... pickable=True,
... extruded=True,
... ),
... pdk.Layer(
... 'ScatterplotLayer',
... data=df,
... get_position='[lon, lat]',
... get_color='[200, 30, 0, 160]',
... get_radius=200,
... ),
... ],
... ))
.. output::
https://static.streamlit.io/0.25.0-2JkNY/index.html?id=ASTdExBpJ1WxbGceneKN1i
height: 530px
"""
pydeck_proto = PydeckProto()
marshall(pydeck_proto, pydeck_obj, use_container_width)
return self.dg._enqueue("deck_gl_json_chart", pydeck_proto)
@property
def dg(self) -> "streamlit.delta_generator.DeltaGenerator":
"""Get our DeltaGenerator."""
return cast("streamlit.delta_generator.DeltaGenerator", self)
# Map used when no data is passed.
EMPTY_MAP: Dict[str, Any] = {
"initialViewState": {"latitude": 0, "longitude": 0, "pitch": 0, "zoom": 1}
}
def marshall(pydeck_proto, pydeck_obj, use_container_width):
if pydeck_obj is None:
spec = json.dumps(EMPTY_MAP)
else:
spec = pydeck_obj.to_json()
pydeck_proto.json = spec
pydeck_proto.use_container_width = use_container_width
if pydeck_obj is not None and isinstance(pydeck_obj.deck_widget.tooltip, dict):
pydeck_proto.tooltip = json.dumps(pydeck_obj.deck_widget.tooltip)

View File

@@ -0,0 +1,166 @@
# Copyright 2018-2022 Streamlit Inc.
#
# 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.
"""Allows us to create and absorb changes (aka Deltas) to elements."""
import inspect
from typing import cast
import streamlit
from streamlit.proto.DocString_pb2 import DocString as DocStringProto
from streamlit.logger import get_logger
LOGGER = get_logger(__name__)
CONFUSING_STREAMLIT_MODULES = (
"streamlit.echo",
"streamlit.delta_generator",
"streamlit.legacy_caching.caching",
)
CONFUSING_STREAMLIT_SIG_PREFIXES = ("(element, ",)
class HelpMixin:
def help(self, obj):
"""Display object's doc string, nicely formatted.
Displays the doc string for this object.
Parameters
----------
obj : Object
The object whose docstring should be displayed.
Example
-------
Don't remember how to initialize a dataframe? Try this:
>>> st.help(pandas.DataFrame)
Want to quickly check what datatype is output by a certain function?
Try:
>>> x = my_poorly_documented_function()
>>> st.help(x)
"""
doc_string_proto = DocStringProto()
_marshall(doc_string_proto, obj)
return self.dg._enqueue("doc_string", doc_string_proto)
@property
def dg(self) -> "streamlit.delta_generator.DeltaGenerator":
"""Get our DeltaGenerator."""
return cast("streamlit.delta_generator.DeltaGenerator", self)
def _marshall(doc_string_proto, obj):
"""Construct a DocString object.
See DeltaGenerator.help for docs.
"""
try:
doc_string_proto.name = obj.__name__
except AttributeError:
pass
module_name = getattr(obj, "__module__", None)
if module_name in CONFUSING_STREAMLIT_MODULES:
doc_string_proto.module = "streamlit"
elif module_name is not None:
doc_string_proto.module = module_name
else:
# Leave doc_string_proto.module as an empty string (default value).
pass
obj_type = type(obj)
doc_string_proto.type = str(obj_type)
if callable(obj):
doc_string_proto.signature = _get_signature(obj)
doc_string = inspect.getdoc(obj)
# Sometimes an object has no docstring, but the object's type does.
# If that's the case here, use the type's docstring.
# For objects where type is type we do not print the docs.
# We also do not print the docs for functions and methods if
# the docstring is empty.
if (
doc_string is None
and obj_type is not type
and not inspect.isfunction(obj)
and not inspect.ismethod(obj)
):
doc_string = inspect.getdoc(obj_type)
if doc_string is None:
doc_string = "No docs available."
doc_string_proto.doc_string = doc_string
def _get_signature(f):
is_delta_gen = False
try:
is_delta_gen = f.__module__ == "streamlit.delta_generator"
if is_delta_gen:
# DeltaGenerator functions are doubly wrapped, and their function
# signatures are useless unless we unwrap them.
f = _unwrap_decorated_func(f)
# Functions such as numpy.minimum don't have a __module__ attribute,
# since we're only using it to check if its a DeltaGenerator, its ok
# to continue
except AttributeError:
pass
sig = ""
try:
sig = str(inspect.signature(f))
except ValueError:
# f is a builtin.
pass
if is_delta_gen:
for prefix in CONFUSING_STREAMLIT_SIG_PREFIXES:
if sig.startswith(prefix):
sig = sig.replace(prefix, "(")
break
return sig
def _unwrap_decorated_func(f):
if hasattr(f, "__wrapped__"):
try:
while getattr(f, "__wrapped__"):
contents = f.__wrapped__
if not callable(contents):
break
f = contents
return f
except AttributeError:
pass
# Fall back to original function, though it's unlikely we'll reach
# this part of the code.
return f

View File

@@ -0,0 +1,71 @@
# Copyright 2018-2022 Streamlit Inc.
#
# 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 typing import cast
import streamlit
from streamlit.proto.Empty_pb2 import Empty as EmptyProto
class EmptyMixin:
def empty(self):
"""Insert a single-element container.
Inserts a container into your app that can be used to hold a single element.
This allows you to, for example, remove elements at any point, or replace
several elements at once (using a child multi-element container).
To insert/replace/clear an element on the returned container, you can
use "with" notation or just call methods directly on the returned object.
See examples below.
Examples
--------
Overwriting elements in-place using "with" notation:
>>> import time
>>>
>>> with st.empty():
... for seconds in range(60):
... st.write(f"{seconds} seconds have passed")
... time.sleep(1)
... st.write("✔️ 1 minute over!")
Replacing several elements, then clearing them:
>>> placeholder = st.empty()
>>>
>>> # Replace the placeholder with some text:
>>> placeholder.text("Hello")
>>>
>>> # Replace the text with a chart:
>>> placeholder.line_chart({"data": [1, 5, 2, 6]})
>>>
>>> # Replace the chart with several elements:
>>> with placeholder.container():
... st.write("This is one element")
... st.write("This is another")
...
>>> # Clear all those elements:
>>> placeholder.empty()
"""
empty_proto = EmptyProto()
return self.dg._enqueue("empty", empty_proto)
@property
def dg(self) -> "streamlit.delta_generator.DeltaGenerator":
"""Get our DeltaGenerator."""
return cast("streamlit.delta_generator.DeltaGenerator", self)

View File

@@ -0,0 +1,250 @@
# Copyright 2018-2022 Streamlit Inc.
#
# 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.
import os
import traceback
import typing
from typing import Optional, cast, List
import streamlit
from streamlit.proto.Exception_pb2 import Exception as ExceptionProto
from streamlit.errors import MarkdownFormattedException
from streamlit.errors import StreamlitAPIException
from streamlit.errors import StreamlitAPIWarning
from streamlit.errors import StreamlitDeprecationWarning
from streamlit.errors import UncaughtAppException
from streamlit.logger import get_logger
LOGGER = get_logger(__name__)
# When client.showErrorDetails is False, we show a generic warning in the
# frontend when we encounter an uncaught app exception.
_GENERIC_UNCAUGHT_EXCEPTION_TEXT = "This app has encountered an error. The original error message is redacted to prevent data leaks. Full error details have been recorded in the logs (if you're on Streamlit Cloud, click on 'Manage app' in the lower right of your app)."
# Extract the streamlit package path
_STREAMLIT_DIR = os.path.dirname(streamlit.__file__)
# Make it absolute, resolve aliases, and ensure there's a trailing path
# separator
_STREAMLIT_DIR = os.path.join(os.path.realpath(_STREAMLIT_DIR), "")
class ExceptionMixin:
def exception(self, exception):
"""Display an exception.
Parameters
----------
exception : Exception
The exception to display.
Example
-------
>>> e = RuntimeError('This is an exception of type RuntimeError')
>>> st.exception(e)
"""
exception_proto = ExceptionProto()
marshall(exception_proto, exception)
return self.dg._enqueue("exception", exception_proto)
@property
def dg(self) -> "streamlit.delta_generator.DeltaGenerator":
"""Get our DeltaGenerator."""
return cast("streamlit.delta_generator.DeltaGenerator", self)
def marshall(exception_proto: ExceptionProto, exception: BaseException) -> None:
"""Marshalls an Exception.proto message.
Parameters
----------
exception_proto : Exception.proto
The Exception protobuf to fill out
exception : BaseException
The exception whose data we're extracting
"""
# If this is a StreamlitAPIException, we prune all Streamlit entries
# from the exception's stack trace.
is_api_exception = isinstance(exception, StreamlitAPIException)
is_deprecation_exception = isinstance(exception, StreamlitDeprecationWarning)
is_markdown_exception = isinstance(exception, MarkdownFormattedException)
is_uncaught_app_exception = isinstance(exception, UncaughtAppException)
stack_trace = (
[]
if is_deprecation_exception
else _get_stack_trace_str_list(
exception, strip_streamlit_stack_entries=is_api_exception
)
)
# Some exceptions (like UserHashError) have an alternate_name attribute so
# we can pretend to the user that the exception is called something else.
if getattr(exception, "alternate_name", None) is not None:
exception_proto.type = getattr(exception, "alternate_name")
else:
exception_proto.type = type(exception).__name__
exception_proto.stack_trace.extend(stack_trace)
exception_proto.is_warning = isinstance(exception, Warning)
try:
if isinstance(exception, SyntaxError):
# SyntaxErrors have additional fields (filename, text, lineno,
# offset) that we can use for a nicely-formatted message telling
# the user what to fix.
exception_proto.message = _format_syntax_error_message(exception)
else:
exception_proto.message = str(exception).strip()
exception_proto.message_is_markdown = is_markdown_exception
except Exception as str_exception:
# Sometimes the exception's __str__/__unicode__ method itself
# raises an error.
exception_proto.message = ""
LOGGER.warning(
"""
Streamlit was unable to parse the data from an exception in the user's script.
This is usually due to a bug in the Exception object itself. Here is some info
about that Exception object, so you can report a bug to the original author:
Exception type:
%(etype)s
Problem:
%(str_exception)s
Traceback:
%(str_exception_tb)s
"""
% {
"etype": type(exception).__name__,
"str_exception": str_exception,
"str_exception_tb": "\n".join(_get_stack_trace_str_list(str_exception)),
}
)
if is_uncaught_app_exception:
uae = typing.cast(UncaughtAppException, exception)
exception_proto.message = _GENERIC_UNCAUGHT_EXCEPTION_TEXT
type_str = str(type(uae.exc))
exception_proto.type = type_str.replace("<class '", "").replace("'>", "")
def _format_syntax_error_message(exception: SyntaxError) -> str:
"""Returns a nicely formatted SyntaxError message that emulates
what the Python interpreter outputs, e.g.:
> File "raven.py", line 3
> st.write('Hello world!!'))
> ^
> SyntaxError: invalid syntax
"""
if exception.text:
if exception.offset is not None:
caret_indent = " " * max(exception.offset - 1, 0)
else:
caret_indent = ""
return (
'File "%(filename)s", line %(lineno)s\n'
" %(text)s\n"
" %(caret_indent)s^\n"
"%(errname)s: %(msg)s"
% {
"filename": exception.filename,
"lineno": exception.lineno,
"text": exception.text.rstrip(),
"caret_indent": caret_indent,
"errname": type(exception).__name__,
"msg": exception.msg,
}
)
# If a few edge cases, SyntaxErrors don't have all these nice fields. So we
# have a fall back here.
# Example edge case error message: encoding declaration in Unicode string
return str(exception)
def _get_stack_trace_str_list(
exception: BaseException, strip_streamlit_stack_entries: bool = False
) -> List[str]:
"""Get the stack trace for the given exception.
Parameters
----------
exception : BaseException
The exception to extract the traceback from
strip_streamlit_stack_entries : bool
If True, all traceback entries that are in the Streamlit package
will be removed from the list. We do this for exceptions that result
from incorrect usage of Streamlit APIs, so that the user doesn't see
a bunch of noise about ScriptRunner, DeltaGenerator, etc.
Returns
-------
list
The exception traceback as a list of strings
"""
extracted_traceback: Optional[traceback.StackSummary] = None
if isinstance(exception, StreamlitAPIWarning):
extracted_traceback = exception.tacked_on_stack
elif hasattr(exception, "__traceback__"):
extracted_traceback = traceback.extract_tb(exception.__traceback__)
if isinstance(exception, UncaughtAppException):
extracted_traceback = traceback.extract_tb(exception.exc.__traceback__)
# Format the extracted traceback and add it to the protobuf element.
if extracted_traceback is None:
stack_trace_str_list = [
"Cannot extract the stack trace for this exception. "
"Try calling exception() within the `catch` block."
]
else:
if strip_streamlit_stack_entries:
extracted_frames = _get_nonstreamlit_traceback(extracted_traceback)
stack_trace_str_list = traceback.format_list(extracted_frames)
else:
stack_trace_str_list = traceback.format_list(extracted_traceback)
stack_trace_str_list = [item.strip() for item in stack_trace_str_list]
return stack_trace_str_list
def _is_in_streamlit_package(file: str) -> bool:
"""True if the given file is part of the streamlit package."""
try:
common_prefix = os.path.commonprefix([os.path.realpath(file), _STREAMLIT_DIR])
except ValueError:
# Raised if paths are on different drives.
return False
return common_prefix == _STREAMLIT_DIR
def _get_nonstreamlit_traceback(
extracted_tb: traceback.StackSummary,
) -> List[traceback.FrameSummary]:
return [
entry for entry in extracted_tb if not _is_in_streamlit_package(entry.filename)
]

View File

@@ -0,0 +1,397 @@
# Copyright 2018-2022 Streamlit Inc.
#
# 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 streamlit.type_util import Key, to_key
from typing import cast, overload, List, Optional, Union
from textwrap import dedent
import sys
if sys.version_info >= (3, 8):
from typing import Literal
else:
from typing_extensions import Literal
import streamlit
from streamlit import config
from streamlit.logger import get_logger
from streamlit.proto.FileUploader_pb2 import FileUploader as FileUploaderProto
from streamlit.scriptrunner import ScriptRunContext, get_script_run_ctx
from streamlit.state import (
register_widget,
WidgetArgs,
WidgetCallback,
WidgetKwargs,
)
from .form import current_form_id
from ..proto.Common_pb2 import (
FileUploaderState as FileUploaderStateProto,
UploadedFileInfo as UploadedFileInfoProto,
)
from ..uploaded_file_manager import UploadedFile, UploadedFileRec
from .utils import check_callback_rules, check_session_state_rules
LOGGER = get_logger(__name__)
SomeUploadedFiles = Optional[Union[UploadedFile, List[UploadedFile]]]
class FileUploaderMixin:
# Multiple overloads are defined on `file_uploader()` below to represent
# the different return types of `file_uploader()`.
# These return types differ according to the value of the `accept_multiple_files` argument.
# There are 2 associated variables, each with 2 options.
# 1. The `accept_multiple_files` argument is set as `True`,
# or it is set as `False` or omitted, in which case the default value `False`.
# 2. The `type` argument may or may not be provided as a keyword-only argument.
# There must be 2x2=4 overloads to cover all the possible arguments,
# as these overloads must be mutually exclusive for mypy.
# 1. type is given as not a keyword-only argument
# 2. accept_multiple_files = True
@overload
def file_uploader(
self,
label: str,
type: Optional[Union[str, List[str]]],
accept_multiple_files: Literal[True],
key: Optional[Key] = None,
help: Optional[str] = None,
on_change: Optional[WidgetCallback] = None,
args: Optional[WidgetArgs] = None,
kwargs: Optional[WidgetKwargs] = None,
*,
disabled: bool = False,
) -> Optional[List[UploadedFile]]:
...
# 1. type is given as not a keyword-only argument
# 2. accept_multiple_files = False or omitted
@overload
def file_uploader(
self,
label: str,
type: Optional[Union[str, List[str]]],
accept_multiple_files: Literal[False] = False,
key: Optional[Key] = None,
help: Optional[str] = None,
on_change: Optional[WidgetCallback] = None,
args: Optional[WidgetArgs] = None,
kwargs: Optional[WidgetKwargs] = None,
*,
disabled: bool = False,
) -> Optional[UploadedFile]:
...
# The following 2 overloads represent the cases where
# the `type` argument is a keyword-only argument.
# See https://github.com/python/mypy/issues/4020#issuecomment-737600893
# for the related discussions and examples.
# 1. type is skipped or a keyword argument
# 2. accept_multiple_files = True
@overload
def file_uploader(
self,
label: str,
*,
accept_multiple_files: Literal[True],
type: Optional[Union[str, List[str]]] = None,
key: Optional[Key] = None,
help: Optional[str] = None,
on_change: Optional[WidgetCallback] = None,
args: Optional[WidgetArgs] = None,
kwargs: Optional[WidgetKwargs] = None,
disabled: bool = False,
) -> Optional[List[UploadedFile]]:
...
# 1. type is skipped or a keyword argument
# 2. accept_multiple_files = False or omitted
@overload
def file_uploader(
self,
label: str,
*,
accept_multiple_files: Literal[False] = False,
type: Optional[Union[str, List[str]]] = None,
key: Optional[Key] = None,
help: Optional[str] = None,
on_change: Optional[WidgetCallback] = None,
args: Optional[WidgetArgs] = None,
kwargs: Optional[WidgetKwargs] = None,
disabled: bool = False,
) -> Optional[UploadedFile]:
...
def file_uploader(
self,
label: str,
type: Optional[Union[str, List[str]]] = None,
accept_multiple_files: bool = False,
key: Optional[Key] = None,
help: Optional[str] = None,
on_change: Optional[WidgetCallback] = None,
args: Optional[WidgetArgs] = None,
kwargs: Optional[WidgetKwargs] = None,
*, # keyword-only arguments:
disabled: bool = False,
):
"""Display a file uploader widget.
By default, uploaded files are limited to 200MB. You can configure
this using the `server.maxUploadSize` config option. For more info
on how to set config options, see
https://docs.streamlit.io/library/advanced-features/configuration#set-configuration-options
Parameters
----------
label : str
A short label explaining to the user what this file uploader is for.
type : str or list of str or None
Array of allowed extensions. ['png', 'jpg']
The default is None, which means all extensions are allowed.
accept_multiple_files : bool
If True, allows the user to upload multiple files at the same time,
in which case the return value will be a list of files.
Default: False
key : str or int
An optional string or integer to use as the unique key for the widget.
If this is omitted, a key will be generated for the widget
based on its content. Multiple widgets of the same type may
not share the same key.
help : str
A tooltip that gets displayed next to the file uploader.
on_change : callable
An optional callback invoked when this file_uploader's value
changes.
args : tuple
An optional tuple of args to pass to the callback.
kwargs : dict
An optional dict of kwargs to pass to the callback.
disabled : bool
An optional boolean, which disables the file uploader if set to
True. The default is False. This argument can only be supplied by
keyword.
Returns
-------
None or UploadedFile or list of UploadedFile
- If accept_multiple_files is False, returns either None or
an UploadedFile object.
- If accept_multiple_files is True, returns a list with the
uploaded files as UploadedFile objects. If no files were
uploaded, returns an empty list.
The UploadedFile class is a subclass of BytesIO, and therefore
it is "file-like". This means you can pass them anywhere where
a file is expected.
Examples
--------
Insert a file uploader that accepts a single file at a time:
>>> uploaded_file = st.file_uploader("Choose a file")
>>> if uploaded_file is not None:
... # To read file as bytes:
... bytes_data = uploaded_file.getvalue()
... st.write(bytes_data)
>>>
... # To convert to a string based IO:
... stringio = StringIO(uploaded_file.getvalue().decode("utf-8"))
... st.write(stringio)
>>>
... # To read file as string:
... string_data = stringio.read()
... st.write(string_data)
>>>
... # Can be used wherever a "file-like" object is accepted:
... dataframe = pd.read_csv(uploaded_file)
... st.write(dataframe)
Insert a file uploader that accepts multiple files at a time:
>>> uploaded_files = st.file_uploader("Choose a CSV file", accept_multiple_files=True)
>>> for uploaded_file in uploaded_files:
... bytes_data = uploaded_file.read()
... st.write("filename:", uploaded_file.name)
... st.write(bytes_data)
.. output::
https://share.streamlit.io/streamlit/docs/main/python/api-examples-source/widget.file_uploader.py
height: 375px
"""
ctx = get_script_run_ctx()
return self._file_uploader(
label=label,
type=type,
accept_multiple_files=accept_multiple_files,
key=key,
help=help,
on_change=on_change,
args=args,
kwargs=kwargs,
disabled=disabled,
ctx=ctx,
)
def _file_uploader(
self,
label: str,
type: Optional[Union[str, List[str]]] = None,
accept_multiple_files: bool = False,
key: Optional[Key] = None,
help: Optional[str] = None,
on_change: Optional[WidgetCallback] = None,
args: Optional[WidgetArgs] = None,
kwargs: Optional[WidgetKwargs] = None,
*, # keyword-only arguments:
disabled: bool = False,
ctx: Optional[ScriptRunContext] = None,
):
key = to_key(key)
check_callback_rules(self.dg, on_change)
check_session_state_rules(default_value=None, key=key, writes_allowed=False)
if type:
if isinstance(type, str):
type = [type]
# May need a regex or a library to validate file types are valid
# extensions.
type = [
file_type if file_type[0] == "." else f".{file_type}"
for file_type in type
]
file_uploader_proto = FileUploaderProto()
file_uploader_proto.label = label
file_uploader_proto.type[:] = type if type is not None else []
file_uploader_proto.max_upload_size_mb = config.get_option(
"server.maxUploadSize"
)
file_uploader_proto.multiple_files = accept_multiple_files
file_uploader_proto.form_id = current_form_id(self.dg)
if help is not None:
file_uploader_proto.help = dedent(help)
def deserialize_file_uploader(
ui_value: Optional[FileUploaderStateProto], widget_id: str
) -> SomeUploadedFiles:
file_recs = self._get_file_recs(widget_id, ui_value)
if len(file_recs) == 0:
return_value: Optional[Union[List[UploadedFile], UploadedFile]] = (
[] if accept_multiple_files else None
)
else:
files = [UploadedFile(rec) for rec in file_recs]
return_value = files if accept_multiple_files else files[0]
return return_value
def serialize_file_uploader(files: SomeUploadedFiles) -> FileUploaderStateProto:
state_proto = FileUploaderStateProto()
ctx = get_script_run_ctx()
if ctx is None:
return state_proto
# ctx.uploaded_file_mgr._file_id_counter stores the id to use for
# the *next* uploaded file, so the current highest file id is the
# counter minus 1.
state_proto.max_file_id = ctx.uploaded_file_mgr._file_id_counter - 1
if not files:
return state_proto
elif not isinstance(files, list):
files = [files]
for f in files:
file_info: UploadedFileInfoProto = state_proto.uploaded_file_info.add()
file_info.id = f.id
file_info.name = f.name
file_info.size = f.size
return state_proto
# FileUploader's widget value is a list of file IDs
# representing the current set of files that this uploader should
# know about.
widget_value, _ = register_widget(
"file_uploader",
file_uploader_proto,
user_key=key,
on_change_handler=on_change,
args=args,
kwargs=kwargs,
deserializer=deserialize_file_uploader,
serializer=serialize_file_uploader,
ctx=ctx,
)
# This needs to be done after register_widget because we don't want
# the following proto fields to affect a widget's ID.
file_uploader_proto.disabled = disabled
file_uploader_state = serialize_file_uploader(widget_value)
uploaded_file_info = file_uploader_state.uploaded_file_info
if ctx is not None and len(uploaded_file_info) != 0:
newest_file_id = file_uploader_state.max_file_id
active_file_ids = [f.id for f in uploaded_file_info]
ctx.uploaded_file_mgr.remove_orphaned_files(
session_id=ctx.session_id,
widget_id=file_uploader_proto.id,
newest_file_id=newest_file_id,
active_file_ids=active_file_ids,
)
self.dg._enqueue("file_uploader", file_uploader_proto)
return cast(SomeUploadedFiles, widget_value)
@staticmethod
def _get_file_recs(
widget_id: str, widget_value: Optional[FileUploaderStateProto]
) -> List[UploadedFileRec]:
if widget_value is None:
return []
ctx = get_script_run_ctx()
if ctx is None:
return []
uploaded_file_info = widget_value.uploaded_file_info
if len(uploaded_file_info) == 0:
return []
active_file_ids = [f.id for f in uploaded_file_info]
# Grab the files that correspond to our active file IDs.
return ctx.uploaded_file_mgr.get_files(
session_id=ctx.session_id,
widget_id=widget_id,
file_ids=active_file_ids,
)
@property
def dg(self) -> "streamlit.delta_generator.DeltaGenerator":
"""Get our DeltaGenerator."""
return cast("streamlit.delta_generator.DeltaGenerator", self)

View File

@@ -0,0 +1,274 @@
# Copyright 2018-2022 Streamlit Inc.
#
# 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.
import textwrap
from typing import cast, Optional, NamedTuple
import streamlit
from streamlit.errors import StreamlitAPIException
from streamlit.proto import Block_pb2
from streamlit.scriptrunner import ScriptRunContext, get_script_run_ctx
class FormData(NamedTuple):
"""Form data stored on a DeltaGenerator."""
# The form's unique ID.
form_id: str
def _current_form(
this_dg: "streamlit.delta_generator.DeltaGenerator",
) -> Optional[FormData]:
"""Find the FormData for the given DeltaGenerator.
Forms are blocks, and can have other blocks nested inside them.
To find the current form, we walk up the dg_stack until we find
a DeltaGenerator that has FormData.
"""
if not streamlit._is_running_with_streamlit:
return None
if this_dg._form_data is not None:
return this_dg._form_data
if this_dg == this_dg._main_dg:
# We were created via an `st.foo` call.
# Walk up the dg_stack to see if we're nested inside a `with st.form` statement.
ctx = get_script_run_ctx()
if ctx is None or len(ctx.dg_stack) == 0:
return None
for dg in reversed(ctx.dg_stack):
if dg._form_data is not None:
return dg._form_data
else:
# We were created via an `dg.foo` call.
# Take a look at our parent's form data to see if we're nested inside a form.
parent = this_dg._parent
if parent is not None and parent._form_data is not None:
return parent._form_data
return None
def current_form_id(dg: "streamlit.delta_generator.DeltaGenerator") -> str:
"""Return the form_id for the current form, or the empty string if we're
not inside an `st.form` block.
(We return the empty string, instead of None, because this value is
assigned to protobuf message fields, and None is not valid.)
"""
form_data = _current_form(dg)
if form_data is None:
return ""
return form_data.form_id
def is_in_form(dg: "streamlit.delta_generator.DeltaGenerator") -> bool:
"""True if the DeltaGenerator is inside an st.form block."""
return current_form_id(dg) != ""
def _build_duplicate_form_message(user_key: Optional[str] = None) -> str:
if user_key is not None:
message = textwrap.dedent(
f"""
There are multiple identical forms with `key='{user_key}'`.
To fix this, please make sure that the `key` argument is unique for
each `st.form` you create.
"""
)
else:
message = textwrap.dedent(
"""
There are multiple identical forms with the same generated key.
When a form is created, it's assigned an internal key based on
its structure. Multiple forms with an identical structure will
result in the same internal key, which causes this error.
To fix this error, please pass a unique `key` argument to
`st.form`.
"""
)
return message.strip("\n")
class FormMixin:
def form(self, key: str, clear_on_submit: bool = False):
"""Create a form that batches elements together with a "Submit" button.
A form is a container that visually groups other elements and
widgets together, and contains a Submit button. When the form's
Submit button is pressed, all widget values inside the form will be
sent to Streamlit in a batch.
To add elements to a form object, you can use "with" notation
(preferred) or just call methods directly on the form. See
examples below.
Forms have a few constraints:
* Every form must contain a ``st.form_submit_button``.
* ``st.button`` and ``st.download_button`` cannot be added to a form.
* Forms can appear anywhere in your app (sidebar, columns, etc),
but they cannot be embedded inside other forms.
For more information about forms, check out our
`blog post <https://blog.streamlit.io/introducing-submit-button-and-forms/>`_.
Parameters
----------
key : str
A string that identifies the form. Each form must have its own
key. (This key is not displayed to the user in the interface.)
clear_on_submit : bool
If True, all widgets inside the form will be reset to their default
values after the user presses the Submit button. Defaults to False.
(Note that Custom Components are unaffected by this flag, and
will not be reset to their defaults on form submission.)
Examples
--------
Inserting elements using "with" notation:
>>> with st.form("my_form"):
... st.write("Inside the form")
... slider_val = st.slider("Form slider")
... checkbox_val = st.checkbox("Form checkbox")
...
... # Every form must have a submit button.
... submitted = st.form_submit_button("Submit")
... if submitted:
... st.write("slider", slider_val, "checkbox", checkbox_val)
...
>>> st.write("Outside the form")
Inserting elements out of order:
>>> form = st.form("my_form")
>>> form.slider("Inside the form")
>>> st.slider("Outside the form")
>>>
>>> # Now add a submit button to the form:
>>> form.form_submit_button("Submit")
"""
from .utils import check_session_state_rules
if is_in_form(self.dg):
raise StreamlitAPIException("Forms cannot be nested in other forms.")
check_session_state_rules(default_value=None, key=key, writes_allowed=False)
# A form is uniquely identified by its key.
form_id = key
ctx = get_script_run_ctx()
if ctx is not None:
new_form_id = form_id not in ctx.form_ids_this_run
if new_form_id:
ctx.form_ids_this_run.add(form_id)
else:
raise StreamlitAPIException(_build_duplicate_form_message(key))
block_proto = Block_pb2.Block()
block_proto.form.form_id = form_id
block_proto.form.clear_on_submit = clear_on_submit
block_dg = self.dg._block(block_proto)
# Attach the form's button info to the newly-created block's
# DeltaGenerator.
block_dg._form_data = FormData(form_id)
return block_dg
def form_submit_button(
self,
label: str = "Submit",
help: Optional[str] = None,
on_click=None,
args=None,
kwargs=None,
) -> bool:
"""Display a form submit button.
When this button is clicked, all widget values inside the form will be
sent to Streamlit in a batch.
Every form must have a form_submit_button. A form_submit_button
cannot exist outside a form.
For more information about forms, check out our
`blog post <https://blog.streamlit.io/introducing-submit-button-and-forms/>`_.
Parameters
----------
label : str
A short label explaining to the user what this button is for.
Defaults to "Submit".
help : str or None
A tooltip that gets displayed when the button is hovered over.
Defaults to None.
on_click : callable
An optional callback invoked when this button is clicked.
args : tuple
An optional tuple of args to pass to the callback.
kwargs : dict
An optional dict of kwargs to pass to the callback.
Returns
-------
bool
True if the button was clicked.
"""
ctx = get_script_run_ctx()
return self._form_submit_button(
label=label,
help=help,
on_click=on_click,
args=args,
kwargs=kwargs,
ctx=ctx,
)
def _form_submit_button(
self,
label: str = "Submit",
help: Optional[str] = None,
on_click=None,
args=None,
kwargs=None,
ctx: Optional[ScriptRunContext] = None,
) -> bool:
form_id = current_form_id(self.dg)
submit_button_key = f"FormSubmitter:{form_id}-{label}"
return self.dg._button(
label=label,
key=submit_button_key,
help=help,
is_form_submitter=True,
on_click=on_click,
args=args,
kwargs=kwargs,
ctx=ctx,
)
@property
def dg(self) -> "streamlit.delta_generator.DeltaGenerator":
"""Get our DeltaGenerator."""
return cast("streamlit.delta_generator.DeltaGenerator", self)

View File

@@ -0,0 +1,124 @@
# Copyright 2018-2022 Streamlit Inc.
#
# 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 GraphViz charts."""
import hashlib
from typing import cast
import streamlit
from streamlit import type_util
from streamlit.errors import StreamlitAPIException
from streamlit.logger import get_logger
from streamlit.proto.GraphVizChart_pb2 import GraphVizChart as GraphVizChartProto
LOGGER = get_logger(__name__)
class GraphvizMixin:
def graphviz_chart(self, figure_or_dot, use_container_width=False):
"""Display a graph using the dagre-d3 library.
Parameters
----------
figure_or_dot : graphviz.dot.Graph, graphviz.dot.Digraph, str
The Graphlib graph object or dot string to display
use_container_width : bool
If True, set the chart width to the column width. This takes
precedence over the figure's native `width` value.
Example
-------
>>> import streamlit as st
>>> import graphviz as graphviz
>>>
>>> # Create a graphlib graph object
>>> graph = graphviz.Digraph()
>>> graph.edge('run', 'intr')
>>> graph.edge('intr', 'runbl')
>>> graph.edge('runbl', 'run')
>>> graph.edge('run', 'kernel')
>>> graph.edge('kernel', 'zombie')
>>> graph.edge('kernel', 'sleep')
>>> graph.edge('kernel', 'runmem')
>>> graph.edge('sleep', 'swap')
>>> graph.edge('swap', 'runswap')
>>> graph.edge('runswap', 'new')
>>> graph.edge('runswap', 'runmem')
>>> graph.edge('new', 'runmem')
>>> graph.edge('sleep', 'runmem')
>>>
>>> st.graphviz_chart(graph)
Or you can render the chart from the graph using GraphViz's Dot
language:
>>> st.graphviz_chart('''
digraph {
run -> intr
intr -> runbl
runbl -> run
run -> kernel
kernel -> zombie
kernel -> sleep
kernel -> runmem
sleep -> swap
swap -> runswap
runswap -> new
runswap -> runmem
new -> runmem
sleep -> runmem
}
''')
.. output::
https://share.streamlit.io/streamlit/docs/main/python/api-examples-source/charts.graphviz_chart.py
height: 600px
"""
# Generate element ID from delta path
delta_path = self.dg._get_delta_path_str()
element_id = hashlib.md5(delta_path.encode()).hexdigest()
graphviz_chart_proto = GraphVizChartProto()
marshall(graphviz_chart_proto, figure_or_dot, use_container_width, element_id)
return self.dg._enqueue("graphviz_chart", graphviz_chart_proto)
@property
def dg(self) -> "streamlit.delta_generator.DeltaGenerator":
"""Get our DeltaGenerator."""
return cast("streamlit.delta_generator.DeltaGenerator", self)
def marshall(proto, figure_or_dot, use_container_width, element_id):
"""Construct a GraphViz chart object.
See DeltaGenerator.graphviz_chart for docs.
"""
if type_util.is_graphviz_chart(figure_or_dot):
dot = figure_or_dot.source
elif isinstance(figure_or_dot, str):
dot = figure_or_dot
else:
raise StreamlitAPIException(
"Unhandled type for graphviz chart: %s" % type(figure_or_dot)
)
proto.spec = dot
proto.use_container_width = use_container_width
proto.element_id = element_id

View File

@@ -0,0 +1,142 @@
# Copyright 2018-2022 Streamlit Inc.
#
# 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 typing import Optional
from typing import cast
import streamlit
from streamlit.proto.IFrame_pb2 import IFrame as IFrameProto
class IframeMixin:
def _iframe(
self,
src,
width=None,
height=None,
scrolling=False,
):
"""Load a remote URL in an iframe.
Parameters
----------
src : str
The URL of the page to embed.
width : int
The width of the frame in CSS pixels. Defaults to the app's
default element width.
height : int
The height of the frame in CSS pixels. Defaults to 150.
scrolling : bool
If True, show a scrollbar when the content is larger than the iframe.
Otherwise, do not show a scrollbar. Defaults to False.
"""
iframe_proto = IFrameProto()
marshall(
iframe_proto,
src=src,
width=width,
height=height,
scrolling=scrolling,
)
return self.dg._enqueue("iframe", iframe_proto)
def _html(
self,
html,
width=None,
height=None,
scrolling=False,
):
"""Display an HTML string in an iframe.
Parameters
----------
html : str
The HTML string to embed in the iframe.
width : int
The width of the frame in CSS pixels. Defaults to the app's
default element width.
height : int
The height of the frame in CSS pixels. Defaults to 150.
scrolling : bool
If True, show a scrollbar when the content is larger than the iframe.
Otherwise, do not show a scrollbar. Defaults to False.
"""
iframe_proto = IFrameProto()
marshall(
iframe_proto,
srcdoc=html,
width=width,
height=height,
scrolling=scrolling,
)
return self.dg._enqueue("iframe", iframe_proto)
@property
def dg(self) -> "streamlit.delta_generator.DeltaGenerator":
"""Get our DeltaGenerator."""
return cast("streamlit.delta_generator.DeltaGenerator", self)
def marshall(
proto,
src: Optional[str] = None,
srcdoc: Optional[str] = None,
width: Optional[int] = None,
height: Optional[int] = None,
scrolling: bool = False,
) -> None:
"""Marshalls data into an IFrame proto.
These parameters correspond directly to <iframe> attributes, which are
described in more detail at
https://developer.mozilla.org/en-US/docs/Web/HTML/Element/iframe.
Parameters
----------
proto : IFrame protobuf
The protobuf object to marshall data into.
src : str
The URL of the page to embed.
srcdoc : str
Inline HTML to embed. Overrides src.
width : int
The width of the frame in CSS pixels. Defaults to the app's
default element width.
height : int
The height of the frame in CSS pixels. Defaults to 150.
scrolling : bool
If true, show a scrollbar when the content is larger than the iframe.
Otherwise, never show a scrollbar.
"""
if src is not None:
proto.src = src
if srcdoc is not None:
proto.srcdoc = srcdoc
if width is not None:
proto.width = width
proto.has_width = True
if height is not None:
proto.height = height
else:
proto.height = 150
proto.scrolling = scrolling

View File

@@ -0,0 +1,374 @@
# Copyright 2018-2022 Streamlit Inc.
#
# 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.
"""Image marshalling."""
import imghdr
import io
import mimetypes
from typing import cast
from urllib.parse import urlparse
import re
import numpy as np
from PIL import Image, ImageFile
import streamlit
from streamlit.errors import StreamlitAPIException
from streamlit.logger import get_logger
from streamlit.in_memory_file_manager import in_memory_file_manager
from streamlit.proto.Image_pb2 import ImageList as ImageListProto
LOGGER = get_logger(__name__)
# This constant is related to the frontend maximum content width specified
# in App.jsx main container
# 730 is the max width of element-container in the frontend, and 2x is for high
# DPI.
MAXIMUM_CONTENT_WIDTH = 2 * 730
class ImageMixin:
def image(
self,
image,
caption=None,
width=None,
use_column_width=None,
clamp=False,
channels="RGB",
output_format="auto",
):
"""Display an image or list of images.
Parameters
----------
image : numpy.ndarray, [numpy.ndarray], BytesIO, str, or [str]
Monochrome image of shape (w,h) or (w,h,1)
OR a color image of shape (w,h,3)
OR an RGBA image of shape (w,h,4)
OR a URL to fetch the image from
OR a path of a local image file
OR an SVG XML string like `<svg xmlns=...</svg>`
OR a list of one of the above, to display multiple images.
caption : str or list of str
Image caption. If displaying multiple images, caption should be a
list of captions (one for each image).
width : int or None
Image width. None means use the image width,
but do not exceed the width of the column.
Should be set for SVG images, as they have no default image width.
use_column_width : 'auto' or 'always' or 'never' or bool
If 'auto', set the image's width to its natural size,
but do not exceed the width of the column.
If 'always' or True, set the image's width to the column width.
If 'never' or False, set the image's width to its natural size.
Note: if set, `use_column_width` takes precedence over the `width` parameter.
clamp : bool
Clamp image pixel values to a valid range ([0-255] per channel).
This is only meaningful for byte array images; the parameter is
ignored for image URLs. If this is not set, and an image has an
out-of-range value, an error will be thrown.
channels : 'RGB' or 'BGR'
If image is an nd.array, this parameter denotes the format used to
represent color information. Defaults to 'RGB', meaning
`image[:, :, 0]` is the red channel, `image[:, :, 1]` is green, and
`image[:, :, 2]` is blue. For images coming from libraries like
OpenCV you should set this to 'BGR', instead.
output_format : 'JPEG', 'PNG', or 'auto'
This parameter specifies the format to use when transferring the
image data. Photos should use the JPEG format for lossy compression
while diagrams should use the PNG format for lossless compression.
Defaults to 'auto' which identifies the compression type based
on the type and format of the image argument.
Example
-------
>>> from PIL import Image
>>> image = Image.open('sunrise.jpg')
>>>
>>> st.image(image, caption='Sunrise by the mountains')
.. output::
https://share.streamlit.io/streamlit/docs/main/python/api-examples-source/charts.image.py
height: 710px
"""
if use_column_width == "auto" or (use_column_width is None and width is None):
width = -3
elif use_column_width == "always" or use_column_width == True:
width = -2
elif width is None:
width = -1
elif width <= 0:
raise StreamlitAPIException("Image width must be positive.")
image_list_proto = ImageListProto()
marshall_images(
self.dg._get_delta_path_str(),
image,
caption,
width,
image_list_proto,
clamp,
channels,
output_format,
)
return self.dg._enqueue("imgs", image_list_proto)
@property
def dg(self) -> "streamlit.delta_generator.DeltaGenerator":
"""Get our DeltaGenerator."""
return cast("streamlit.delta_generator.DeltaGenerator", self)
def _image_may_have_alpha_channel(image):
if image.mode in ("RGBA", "LA", "P"):
return True
else:
return False
def _format_from_image_type(image, output_format):
output_format = output_format.upper()
if output_format == "JPEG" or output_format == "PNG":
return output_format
# We are forgiving on the spelling of JPEG
if output_format == "JPG":
return "JPEG"
if _image_may_have_alpha_channel(image):
return "PNG"
return "JPEG"
def _PIL_to_bytes(image, format="JPEG", quality=100):
tmp = io.BytesIO()
# User must have specified JPEG, so we must convert it
if format == "JPEG" and _image_may_have_alpha_channel(image):
image = image.convert("RGB")
image.save(tmp, format=format, quality=quality)
return tmp.getvalue()
def _BytesIO_to_bytes(data):
data.seek(0)
return data.getvalue()
def _np_array_to_bytes(array, output_format="JPEG"):
img = Image.fromarray(array.astype(np.uint8))
format = _format_from_image_type(img, output_format)
return _PIL_to_bytes(img, format)
def _4d_to_list_3d(array):
return [array[i, :, :, :] for i in range(0, array.shape[0])]
def _verify_np_shape(array):
if len(array.shape) not in (2, 3):
raise StreamlitAPIException("Numpy shape has to be of length 2 or 3.")
if len(array.shape) == 3 and array.shape[-1] not in (1, 3, 4):
raise StreamlitAPIException(
"Channel can only be 1, 3, or 4 got %d. Shape is %s"
% (array.shape[-1], str(array.shape))
)
# If there's only one channel, convert is to x, y
if len(array.shape) == 3 and array.shape[-1] == 1:
array = array[:, :, 0]
return array
def _normalize_to_bytes(data, width, output_format):
image = Image.open(io.BytesIO(data))
actual_width, actual_height = image.size
format = _format_from_image_type(image, output_format)
if output_format.lower() == "auto":
ext = imghdr.what(None, data)
mimetype = mimetypes.guess_type("image.%s" % ext)[0]
# if no other options, attempt to convert
if mimetype is None:
mimetype = "image/" + format.lower()
else:
mimetype = "image/" + format.lower()
if width < 0 and actual_width > MAXIMUM_CONTENT_WIDTH:
width = MAXIMUM_CONTENT_WIDTH
if width > 0 and actual_width > width:
new_height = int(1.0 * actual_height * width / actual_width)
image = image.resize((width, new_height), resample=Image.BILINEAR)
data = _PIL_to_bytes(image, format=format, quality=90)
mimetype = "image/" + format.lower()
return data, mimetype
def _clip_image(image, clamp):
data = image
if issubclass(image.dtype.type, np.floating):
if clamp:
data = np.clip(image, 0, 1.0)
else:
if np.amin(image) < 0.0 or np.amax(image) > 1.0:
raise RuntimeError("Data is outside [0.0, 1.0] and clamp is not set.")
data = data * 255
else:
if clamp:
data = np.clip(image, 0, 255)
else:
if np.amin(image) < 0 or np.amax(image) > 255:
raise RuntimeError("Data is outside [0, 255] and clamp is not set.")
return data
def image_to_url(
image, width, clamp, channels, output_format, image_id, allow_emoji=False
):
# PIL Images
if isinstance(image, ImageFile.ImageFile) or isinstance(image, Image.Image):
format = _format_from_image_type(image, output_format)
data = _PIL_to_bytes(image, format)
# BytesIO
# Note: This doesn't support SVG. We could convert to png (cairosvg.svg2png)
# or just decode BytesIO to string and handle that way.
elif isinstance(image, io.BytesIO):
data = _BytesIO_to_bytes(image)
# Numpy Arrays (ie opencv)
elif type(image) is np.ndarray:
data = _verify_np_shape(image)
data = _clip_image(data, clamp)
if channels == "BGR":
if len(data.shape) == 3:
data = data[:, :, [2, 1, 0]]
else:
raise StreamlitAPIException(
'When using `channels="BGR"`, the input image should '
"have exactly 3 color channels"
)
data = _np_array_to_bytes(data, output_format=output_format)
# Strings
elif isinstance(image, str):
# If it's a url, then set the protobuf and continue
try:
p = urlparse(image)
if p.scheme:
return image
except UnicodeDecodeError:
pass
# Finally, see if it's a file.
try:
with open(image, "rb") as f:
data = f.read()
except:
if allow_emoji:
# This might be an emoji string, so just pass it to the frontend
return image
else:
# Allow OS filesystem errors to raise
raise
# Assume input in bytes.
else:
data = image
(data, mimetype) = _normalize_to_bytes(data, width, output_format)
this_file = in_memory_file_manager.add(data, mimetype, image_id)
return this_file.url
def marshall_images(
coordinates,
image,
caption,
width,
proto_imgs,
clamp,
channels="RGB",
output_format="auto",
):
channels = channels.upper()
# Turn single image and caption into one element list.
if type(image) is list:
images = image
else:
if type(image) == np.ndarray and len(image.shape) == 4:
images = _4d_to_list_3d(image)
else:
images = [image]
if type(caption) is list:
captions = caption
else:
if isinstance(caption, str):
captions = [caption]
# You can pass in a 1-D Numpy array as captions.
elif type(caption) == np.ndarray and len(caption.shape) == 1:
captions = caption.tolist()
# If there are no captions then make the captions list the same size
# as the images list.
elif caption is None:
captions = [None] * len(images)
else:
captions = [str(caption)]
assert type(captions) == list, "If image is a list then caption should be as well"
assert len(captions) == len(images), "Cannot pair %d captions with %d images." % (
len(captions),
len(images),
)
proto_imgs.width = width
# Each image in an image list needs to be kept track of at its own coordinates.
for coord_suffix, (image, caption) in enumerate(zip(images, captions)):
proto_img = proto_imgs.imgs.add()
if caption is not None:
proto_img.caption = str(caption)
# We use the index of the image in the input image list to identify this image inside
# InMemoryFileManager. For this, we just add the index to the image's "coordinates".
image_id = "%s-%i" % (coordinates, coord_suffix)
is_svg = False
if isinstance(image, str):
# Unpack local SVG image file to an SVG string
if image.endswith(".svg") and not image.startswith(("http://", "https://")):
with open(image) as textfile:
image = textfile.read()
# Following regex allows svg image files to start either via a "<?xml...>" tag eventually followed by a "<svg...>" tag or directly starting with a "<svg>" tag
if re.search(r"(^\s?(<\?xml[\s\S]*<svg )|^\s?<svg )", image):
proto_img.markup = f"data:image/svg+xml,{image}"
is_svg = True
if not is_svg:
proto_img.url = image_to_url(
image, width, clamp, channels, output_format, image_id
)

View File

@@ -0,0 +1,85 @@
# Copyright 2018-2022 Streamlit Inc.
#
# 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.
import json
from typing import cast
import streamlit
from streamlit.proto.Json_pb2 import Json as JsonProto
from streamlit.state import SessionStateProxy
class JsonMixin:
def json(
self,
body,
*, # keyword-only arguments:
expanded=True,
):
"""Display object or string as a pretty-printed JSON string.
Parameters
----------
body : Object or str
The object to print as JSON. All referenced objects should be
serializable to JSON as well. If object is a string, we assume it
contains serialized JSON.
expanded : bool
An optional boolean that allows the user to set whether the initial
state of this json element should be expanded. Defaults to True.
This argument can only be supplied by keyword.
Example
-------
>>> st.json({
... 'foo': 'bar',
... 'baz': 'boz',
... 'stuff': [
... 'stuff 1',
... 'stuff 2',
... 'stuff 3',
... 'stuff 5',
... ],
... })
.. output::
https://share.streamlit.io/streamlit/docs/main/python/api-examples-source/data.json.py
height: 385px
"""
import streamlit as st
if isinstance(body, SessionStateProxy):
body = body.to_dict()
if not isinstance(body, str):
try:
body = json.dumps(body, default=repr)
except TypeError as err:
st.warning(
"Warning: this data structure was not fully serializable as "
"JSON due to one or more unexpected keys. (Error was: %s)" % err
)
body = json.dumps(body, skipkeys=True, default=repr)
json_proto = JsonProto()
json_proto.body = body
json_proto.expanded = expanded
return self.dg._enqueue("json", json_proto)
@property
def dg(self) -> "streamlit.delta_generator.DeltaGenerator":
"""Get our DeltaGenerator."""
return cast("streamlit.delta_generator.DeltaGenerator", self)

View File

@@ -0,0 +1,252 @@
# Copyright 2018-2022 Streamlit Inc.
#
# 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 typing import cast, Sequence, Union
from streamlit.beta_util import function_beta_warning
from streamlit.errors import StreamlitAPIException
from streamlit.proto.Block_pb2 import Block as BlockProto
import streamlit
SpecType = Union[int, Sequence[Union[int, float]]]
class LayoutsMixin:
def container(self):
"""Insert a multi-element container.
Inserts an invisible container into your app that can be used to hold
multiple elements. This allows you to, for example, insert multiple
elements into your app out of order.
To add elements to the returned container, you can use "with" notation
(preferred) or just call methods directly on the returned object. See
examples below.
Examples
--------
Inserting elements using "with" notation:
>>> with st.container():
... st.write("This is inside the container")
...
... # You can call any Streamlit command, including custom components:
... st.bar_chart(np.random.randn(50, 3))
...
>>> st.write("This is outside the container")
.. output ::
https://share.streamlit.io/streamlit/docs/main/python/api-examples-source/layout.container1.py
height: 520px
Inserting elements out of order:
>>> container = st.container()
>>> container.write("This is inside the container")
>>> st.write("This is outside the container")
>>>
>>> # Now insert some more in the container
>>> container.write("This is inside too")
.. output ::
https://share.streamlit.io/streamlit/docs/main/python/api-examples-source/layout.container2.py
height: 480px
"""
return self.dg._block()
# TODO: Enforce that columns are not nested or in Sidebar
def columns(self, spec: SpecType):
"""Insert containers laid out as side-by-side columns.
Inserts a number of multi-element containers laid out side-by-side and
returns a list of container objects.
To add elements to the returned containers, you can use "with" notation
(preferred) or just call methods directly on the returned object. See
examples below.
.. warning::
Currently, you may not put columns inside another column.
Parameters
----------
spec : int or list of numbers
If an int
Specifies the number of columns to insert, and all columns
have equal width.
If a list of numbers
Creates a column for each number, and each
column's width is proportional to the number provided. Numbers can
be ints or floats, but they must be positive.
For example, `st.columns([3, 1, 2])` creates 3 columns where
the first column is 3 times the width of the second, and the last
column is 2 times that width.
Returns
-------
list of containers
A list of container objects.
Examples
--------
You can use `with` notation to insert any element into a column:
>>> col1, col2, col3 = st.columns(3)
>>>
>>> with col1:
... st.header("A cat")
... st.image("https://static.streamlit.io/examples/cat.jpg")
...
>>> with col2:
... st.header("A dog")
... st.image("https://static.streamlit.io/examples/dog.jpg")
...
>>> with col3:
... st.header("An owl")
... st.image("https://static.streamlit.io/examples/owl.jpg")
.. output ::
https://share.streamlit.io/streamlit/docs/main/python/api-examples-source/layout.columns1.py
height: 620px
Or you can just call methods directly in the returned objects:
>>> col1, col2 = st.columns([3, 1])
>>> data = np.random.randn(10, 1)
>>>
>>> col1.subheader("A wide column with a chart")
>>> col1.line_chart(data)
>>>
>>> col2.subheader("A narrow column with the data")
>>> col2.write(data)
.. output ::
https://share.streamlit.io/streamlit/docs/main/python/api-examples-source/layout.columns2.py
height: 550px
"""
weights = spec
weights_exception = StreamlitAPIException(
"The input argument to st.columns must be either a "
+ "positive integer or a list of positive numeric weights. "
+ "See [documentation](https://docs.streamlit.io/library/api-reference/layout/st.columns) "
+ "for more information."
)
if isinstance(weights, int):
# If the user provided a single number, expand into equal weights.
# E.g. (1,) * 3 => (1, 1, 1)
# NOTE: A negative/zero spec will expand into an empty tuple.
weights = (1,) * weights
if len(weights) == 0 or any(weight <= 0 for weight in weights):
raise weights_exception
def column_proto(normalized_weight):
col_proto = BlockProto()
col_proto.column.weight = normalized_weight
col_proto.allow_empty = True
return col_proto
block_proto = BlockProto()
block_proto.horizontal.SetInParent()
row = self.dg._block(block_proto)
total_weight = sum(weights)
return [row._block(column_proto(w / total_weight)) for w in weights]
def expander(self, label: str, expanded: bool = False):
"""Insert a multi-element container that can be expanded/collapsed.
Inserts a container into your app that can be used to hold multiple elements
and can be expanded or collapsed by the user. When collapsed, all that is
visible is the provided label.
To add elements to the returned container, you can use "with" notation
(preferred) or just call methods directly on the returned object. See
examples below.
.. warning::
Currently, you may not put expanders inside another expander.
Parameters
----------
label : str
A string to use as the header for the expander.
expanded : bool
If True, initializes the expander in "expanded" state. Defaults to
False (collapsed).
Examples
--------
You can use `with` notation to insert any element into an expander
>>> st.bar_chart({"data": [1, 5, 2, 6, 2, 1]})
>>>
>>> with st.expander("See explanation"):
... st.write(\"\"\"
... The chart above shows some numbers I picked for you.
... I rolled actual dice for these, so they're *guaranteed* to
... be random.
... \"\"\")
... st.image("https://static.streamlit.io/examples/dice.jpg")
.. output ::
https://share.streamlit.io/streamlit/docs/main/python/api-examples-source/layout.expander.py
height: 750px
Or you can just call methods directly in the returned objects:
>>> st.bar_chart({"data": [1, 5, 2, 6, 2, 1]})
>>>
>>> expander = st.expander("See explanation")
>>> expander.write(\"\"\"
... The chart above shows some numbers I picked for you.
... I rolled actual dice for these, so they're *guaranteed* to
... be random.
... \"\"\")
>>> expander.image("https://static.streamlit.io/examples/dice.jpg")
.. output ::
https://share.streamlit.io/streamlit/docs/main/python/api-examples-source/layout.expander.py
height: 750px
"""
if label is None:
raise StreamlitAPIException("A label is required for an expander")
expandable_proto = BlockProto.Expandable()
expandable_proto.expanded = expanded
expandable_proto.label = label
block_proto = BlockProto()
block_proto.allow_empty = True
block_proto.expandable.CopyFrom(expandable_proto)
return self.dg._block(block_proto=block_proto)
@property
def dg(self) -> "streamlit.delta_generator.DeltaGenerator":
"""Get our DeltaGenerator."""
return cast("streamlit.delta_generator.DeltaGenerator", self)
# Deprecated beta_ functions
beta_container = function_beta_warning(container, "2021-11-02")
beta_expander = function_beta_warning(expander, "2021-11-02")
beta_columns = function_beta_warning(columns, "2021-11-02")

View File

@@ -0,0 +1,350 @@
# Copyright 2018-2022 Streamlit Inc.
#
# 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.
"""A Python wrapper around Altair.
Altair is a Python visualization library based on Vega-Lite,
a nice JSON schema for expressing graphs and charts."""
from datetime import date
from typing import cast
import streamlit
from streamlit import errors, type_util
from streamlit.proto.VegaLiteChart_pb2 import VegaLiteChart as VegaLiteChartProto
import streamlit.elements.legacy_vega_lite as vega_lite
import altair as alt
import pandas as pd
import pyarrow as pa
from .utils import last_index_for_melted_dataframes
class LegacyAltairMixin:
def _legacy_line_chart(
self, data=None, width=0, height=0, use_container_width=True
):
"""Display a line chart.
This is syntax-sugar around st._legacy_altair_chart. The main difference
is this command uses the data's own column and indices to figure out
the chart's spec. As a result this is easier to use for many "just plot
this" scenarios, while being less customizable.
If st._legacy_line_chart does not guess the data specification
correctly, try specifying your desired chart using st._legacy_altair_chart.
Parameters
----------
data : pandas.DataFrame, pandas.Styler, numpy.ndarray, Iterable, dict
or None
Data to be plotted.
width : int
The chart width in pixels. If 0, selects the width automatically.
height : int
The chart width in pixels. If 0, selects the height automatically.
use_container_width : bool
If True, set the chart width to the column width. This takes
precedence over the width argument.
Example
-------
>>> chart_data = pd.DataFrame(
... np.random.randn(20, 3),
... columns=['a', 'b', 'c'])
...
>>> st._legacy_line_chart(chart_data)
.. output::
https://static.streamlit.io/0.50.0-td2L/index.html?id=BdxXG3MmrVBfJyqS2R2ki8
height: 220px
"""
vega_lite_chart_proto = VegaLiteChartProto()
chart = generate_chart("line", data, width, height)
marshall(vega_lite_chart_proto, chart, use_container_width)
last_index = last_index_for_melted_dataframes(data)
return self.dg._enqueue(
"line_chart", vega_lite_chart_proto, last_index=last_index
)
def _legacy_area_chart(
self, data=None, width=0, height=0, use_container_width=True
):
"""Display an area chart.
This is just syntax-sugar around st._legacy_altair_chart. The main difference
is this command uses the data's own column and indices to figure out
the chart's spec. As a result this is easier to use for many "just plot
this" scenarios, while being less customizable.
If st._legacy_area_chart does not guess the data specification
correctly, try specifying your desired chart using st._legacy_altair_chart.
Parameters
----------
data : pandas.DataFrame, pandas.Styler, numpy.ndarray, Iterable, or dict
Data to be plotted.
width : int
The chart width in pixels. If 0, selects the width automatically.
height : int
The chart width in pixels. If 0, selects the height automatically.
use_container_width : bool
If True, set the chart width to the column width. This takes
precedence over the width argument.
Example
-------
>>> chart_data = pd.DataFrame(
... np.random.randn(20, 3),
... columns=['a', 'b', 'c'])
...
>>> st._legacy_area_chart(chart_data)
.. output::
https://static.streamlit.io/0.50.0-td2L/index.html?id=Pp65STuFj65cJRDfhGh4Jt
height: 220px
"""
vega_lite_chart_proto = VegaLiteChartProto()
chart = generate_chart("area", data, width, height)
marshall(vega_lite_chart_proto, chart, use_container_width)
last_index = last_index_for_melted_dataframes(data)
return self.dg._enqueue(
"area_chart", vega_lite_chart_proto, last_index=last_index
)
def _legacy_bar_chart(self, data=None, width=0, height=0, use_container_width=True):
"""Display a bar chart.
This is just syntax-sugar around st._legacy_altair_chart. The main difference
is this command uses the data's own column and indices to figure out
the chart's spec. As a result this is easier to use for many "just plot
this" scenarios, while being less customizable.
If st._legacy_bar_chart does not guess the data specification
correctly, try specifying your desired chart using st._legacy_altair_chart.
Parameters
----------
data : pandas.DataFrame, pandas.Styler, numpy.ndarray, Iterable, or dict
Data to be plotted.
width : int
The chart width in pixels. If 0, selects the width automatically.
height : int
The chart width in pixels. If 0, selects the height automatically.
use_container_width : bool
If True, set the chart width to the column width. This takes
precedence over the width argument.
Example
-------
>>> chart_data = pd.DataFrame(
... np.random.randn(50, 3),
... columns=["a", "b", "c"])
...
>>> st._legacy_bar_chart(chart_data)
.. output::
https://static.streamlit.io/0.66.0-2BLtg/index.html?id=GaYDn6vxskvBUkBwsGVEaL
height: 220px
"""
vega_lite_chart_proto = VegaLiteChartProto()
chart = generate_chart("bar", data, width, height)
marshall(vega_lite_chart_proto, chart, use_container_width)
last_index = last_index_for_melted_dataframes(data)
return self.dg._enqueue(
"bar_chart", vega_lite_chart_proto, last_index=last_index
)
def _legacy_altair_chart(self, altair_chart, use_container_width=False):
"""Display a chart using the Altair library.
Parameters
----------
altair_chart : altair.vegalite.v2.api.Chart
The Altair chart object to display.
use_container_width : bool
If True, set the chart width to the column width. This takes
precedence over Altair's native `width` value.
Example
-------
>>> import pandas as pd
>>> import numpy as np
>>> import altair as alt
>>>
>>> df = pd.DataFrame(
... np.random.randn(200, 3),
... columns=['a', 'b', 'c'])
...
>>> c = alt.Chart(df).mark_circle().encode(
... x='a', y='b', size='c', color='c', tooltip=['a', 'b', 'c'])
>>>
>>> st._legacy_altair_chart(c, use_container_width=True)
.. output::
https://static.streamlit.io/0.25.0-2JkNY/index.html?id=8jmmXR8iKoZGV4kXaKGYV5
height: 200px
Examples of Altair charts can be found at
https://altair-viz.github.io/gallery/.
"""
vega_lite_chart_proto = VegaLiteChartProto()
marshall(
vega_lite_chart_proto,
altair_chart,
use_container_width=use_container_width,
)
return self.dg._enqueue("vega_lite_chart", vega_lite_chart_proto)
@property
def dg(self) -> "streamlit.delta_generator.DeltaGenerator":
"""Get our DeltaGenerator."""
return cast("streamlit.delta_generator.DeltaGenerator", self)
def _is_date_column(df, name):
"""True if the column with the given name stores datetime.date values.
This function just checks the first value in the given column, so
it's meaningful only for columns whose values all share the same type.
Parameters
----------
df : pd.DataFrame
name : str
The column name
Returns
-------
bool
"""
column = df[name]
if column.size == 0:
return False
return isinstance(column[0], date)
def generate_chart(chart_type, data, width=0, height=0):
if data is None:
# Use an empty-ish dict because if we use None the x axis labels rotate
# 90 degrees. No idea why. Need to debug.
data = {"": []}
if isinstance(data, pa.Table):
raise errors.StreamlitAPIException(
"""
pyarrow tables are not supported by Streamlit's legacy DataFrame serialization (i.e. with `config.dataFrameSerialization = "legacy"`).
To be able to use pyarrow tables, please enable pyarrow by changing the config setting,
`config.dataFrameSerialization = "arrow"`
"""
)
if not isinstance(data, pd.DataFrame):
data = type_util.convert_anything_to_df(data)
index_name = data.index.name
if index_name is None:
index_name = "index"
data = pd.melt(data.reset_index(), id_vars=[index_name])
if chart_type == "area":
opacity = {"value": 0.7}
else:
opacity = {"value": 1.0}
# Set the X and Y axes' scale to "utc" if they contain date values.
# This causes time data to be displayed in UTC, rather the user's local
# time zone. (By default, vega-lite displays time data in the browser's
# local time zone, regardless of which time zone the data specifies:
# https://vega.github.io/vega-lite/docs/timeunit.html#output).
x_scale = (
alt.Scale(type="utc") if _is_date_column(data, index_name) else alt.Undefined
)
y_scale = alt.Scale(type="utc") if _is_date_column(data, "value") else alt.Undefined
x_type = alt.Undefined
# Bar charts should have a discrete (ordinal) x-axis, UNLESS type is date/time
# https://github.com/streamlit/streamlit/pull/2097#issuecomment-714802475
if chart_type == "bar" and not _is_date_column(data, index_name):
x_type = "ordinal"
chart = (
getattr(alt.Chart(data, width=width, height=height), "mark_" + chart_type)()
.encode(
alt.X(index_name, title="", scale=x_scale, type=x_type),
alt.Y("value", title="", scale=y_scale),
alt.Color("variable", title="", type="nominal"),
alt.Tooltip([index_name, "value", "variable"]),
opacity=opacity,
)
.interactive()
)
return chart
def marshall(vega_lite_chart, altair_chart, use_container_width=False, **kwargs):
import altair as alt
# Normally altair_chart.to_dict() would transform the dataframe used by the
# chart into an array of dictionaries. To avoid that, we install a
# transformer that replaces datasets with a reference by the object id of
# the dataframe. We then fill in the dataset manually later on.
datasets = {}
def id_transform(data):
"""Altair data transformer that returns a fake named dataset with the
object id."""
datasets[id(data)] = data
return {"name": str(id(data))}
alt.data_transformers.register("id", id_transform)
with alt.data_transformers.enable("id"):
chart_dict = altair_chart.to_dict()
# Put datasets back into the chart dict but note how they weren't
# transformed.
chart_dict["datasets"] = datasets
vega_lite.marshall(
vega_lite_chart,
chart_dict,
use_container_width=use_container_width,
**kwargs,
)

View File

@@ -0,0 +1,441 @@
# Copyright 2018-2022 Streamlit Inc.
#
# 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.
"""Helper functions to marshall a pandas.DataFrame into a proto.DataFrame."""
import datetime
import re
from collections import namedtuple
from typing import cast, Dict, Any, Optional
import pyarrow as pa
import tzlocal
from pandas import DataFrame
from pandas.io.formats.style import Styler
import streamlit
from streamlit import errors, type_util
from streamlit.logger import get_logger
from streamlit.proto.DataFrame_pb2 import (
DataFrame as DataFrameProto,
TableStyle as TableStyleProto,
)
LOGGER = get_logger(__name__)
CSSStyle = namedtuple("CSSStyle", ["property", "value"])
class LegacyDataFrameMixin:
def _legacy_dataframe(self, data=None, width=None, height=None):
"""Display a dataframe as an interactive table.
Parameters
----------
data : pandas.DataFrame, pandas.Styler, numpy.ndarray, Iterable, dict,
or None
The data to display.
If 'data' is a pandas.Styler, it will be used to style its
underyling DataFrame. Streamlit supports custom cell
values and colors. (It does not support some of the more exotic
pandas styling features, like bar charts, hovering, and captions.)
Styler support is experimental!
width : int or None
Desired width of the UI element expressed in pixels. If None, a
default width based on the page width is used.
height : int or None
Desired height of the UI element expressed in pixels. If None, a
default height is used.
Examples
--------
>>> df = pd.DataFrame(
... np.random.randn(50, 20),
... columns=('col %d' % i for i in range(20)))
...
>>> st._legacy_dataframe(df)
.. output::
https://static.streamlit.io/0.25.0-2JkNY/index.html?id=165mJbzWdAC8Duf8a4tjyQ
height: 330px
>>> st._legacy_dataframe(df, 200, 100)
You can also pass a Pandas Styler object to change the style of
the rendered DataFrame:
>>> df = pd.DataFrame(
... np.random.randn(10, 20),
... columns=('col %d' % i for i in range(20)))
...
>>> st._legacy_dataframe(df.style.highlight_max(axis=0))
.. output::
https://static.streamlit.io/0.29.0-dV1Y/index.html?id=Hb6UymSNuZDzojUNybzPby
height: 285px
"""
data_frame_proto = DataFrameProto()
marshall_data_frame(data, data_frame_proto)
return self.dg._enqueue(
"data_frame",
data_frame_proto,
element_width=width,
element_height=height,
)
def _legacy_table(self, data=None):
"""Display a static table.
This differs from `st._legacy_dataframe` in that the table in this case is
static: its entire contents are laid out directly on the page.
Parameters
----------
data : pandas.DataFrame, pandas.Styler, numpy.ndarray, Iterable, dict,
or None
The table data.
Example
-------
>>> df = pd.DataFrame(
... np.random.randn(10, 5),
... columns=('col %d' % i for i in range(5)))
...
>>> st._legacy_table(df)
.. output::
https://static.streamlit.io/0.25.0-2JkNY/index.html?id=KfZvDMprL4JFKXbpjD3fpq
height: 480px
"""
table_proto = DataFrameProto()
marshall_data_frame(data, table_proto)
return self.dg._enqueue("table", table_proto)
@property
def dg(self) -> "streamlit.delta_generator.DeltaGenerator":
"""Get our DeltaGenerator."""
return cast("streamlit.delta_generator.DeltaGenerator", self)
def marshall_data_frame(data: Any, proto_df: DataFrameProto) -> None:
"""Convert a pandas.DataFrame into a proto.DataFrame.
Parameters
----------
data : pandas.DataFrame, numpy.ndarray, Iterable, dict, DataFrame, Styler, or None
Something that is or can be converted to a dataframe.
proto_df : proto.DataFrame
Output. The protobuf for a Streamlit DataFrame proto.
"""
if isinstance(data, pa.Table):
raise errors.StreamlitAPIException(
"""
pyarrow tables are not supported by Streamlit's legacy DataFrame serialization (i.e. with `config.dataFrameSerialization = "legacy"`).
To be able to use pyarrow tables, please enable pyarrow by changing the config setting,
`config.dataFrameSerialization = "arrow"`
"""
)
df = type_util.convert_anything_to_df(data)
# Convert df into an iterable of columns (each of type Series).
df_data = (df.iloc[:, col] for col in range(len(df.columns)))
_marshall_table(df_data, proto_df.data)
_marshall_index(df.columns, proto_df.columns)
_marshall_index(df.index, proto_df.index)
styler = data if type_util.is_pandas_styler(data) else None
_marshall_styles(proto_df.style, df, styler)
def _marshall_styles(
proto_table_style: TableStyleProto, df: DataFrame, styler: Optional[Styler] = None
) -> None:
"""Adds pandas.Styler styling data to a proto.DataFrame
Parameters
----------
proto_table_style : proto.TableStyle
df : pandas.DataFrame
styler : pandas.Styler holding styling data for the data frame, or
None if there's no style data to marshall
"""
# NB: we're using protected members of Styler to get this data,
# which is non-ideal and could break if Styler's interface changes.
if styler is not None:
styler._compute()
# In Pandas 1.3.0, styler._translate() signature was changed.
# 2 arguments were added: sparse_index and sparse_columns.
# The functionality that they provide is not yet supported.
if type_util.is_pandas_version_less_than("1.3.0"):
translated_style = styler._translate()
else:
translated_style = styler._translate(False, False)
css_styles = _get_css_styles(translated_style)
display_values = _get_custom_display_values(translated_style)
else:
# If we have no Styler, we just make an empty CellStyle for each cell
css_styles = {}
display_values = {}
nrows, ncols = df.shape
for col in range(ncols):
proto_col = proto_table_style.cols.add()
for row in range(nrows):
proto_cell_style = proto_col.styles.add()
for css in css_styles.get((row, col), []):
proto_css = proto_cell_style.css.add()
proto_css.property = css.property
proto_css.value = css.value
display_value = display_values.get((row, col), None)
if display_value is not None:
proto_cell_style.display_value = display_value
proto_cell_style.has_display_value = True
def _get_css_styles(translated_style: Dict[Any, Any]) -> Dict[Any, Any]:
"""Parses pandas.Styler style dictionary into a
{(row, col): [CSSStyle]} dictionary
"""
# In pandas < 1.1.0
# translated_style["cellstyle"] has the following shape:
# [
# {
# "props": [["color", " black"], ["background-color", "orange"], ["", ""]],
# "selector": "row0_col0"
# }
# ...
# ]
#
# In pandas >= 1.1.0
# translated_style["cellstyle"] has the following shape:
# [
# {
# "props": [("color", " black"), ("background-color", "orange"), ("", "")],
# "selectors": ["row0_col0"]
# }
# ...
# ]
cell_selector_regex = re.compile(r"row(\d+)_col(\d+)")
css_styles = {}
for cell_style in translated_style["cellstyle"]:
if type_util.is_pandas_version_less_than("1.1.0"):
cell_selectors = [cell_style["selector"]]
else:
cell_selectors = cell_style["selectors"]
for cell_selector in cell_selectors:
match = cell_selector_regex.match(cell_selector)
if not match:
raise RuntimeError(
f'Failed to parse cellstyle selector "{cell_selector}"'
)
row = int(match.group(1))
col = int(match.group(2))
css_declarations = []
props = cell_style["props"]
for prop in props:
if not isinstance(prop, (tuple, list)) or len(prop) != 2:
raise RuntimeError(f'Unexpected cellstyle props "{prop}"')
name = str(prop[0]).strip()
value = str(prop[1]).strip()
if name and value:
css_declarations.append(CSSStyle(property=name, value=value))
css_styles[(row, col)] = css_declarations
return css_styles
def _get_custom_display_values(translated_style: Dict[Any, Any]) -> Dict[Any, Any]:
"""Parses pandas.Styler style dictionary into a
{(row, col): display_value} dictionary for cells whose display format
has been customized.
"""
# Create {(row, col): display_value} from translated_style['body']
# translated_style['body'] has the shape:
# [
# [ // row
# { // cell or header
# 'id': 'level0_row0' (for row header) | 'row0_col0' (for cells)
# 'value': 1.329212
# 'display_value': '132.92%'
# ...
# }
# ]
# ]
def has_custom_display_value(cell: Dict[Any, Any]) -> bool:
# We'd prefer to only pass `display_value` data to the frontend
# when a DataFrame cell has been custom-formatted by the user, to
# save on bandwidth. However:
#
# Panda's Styler's internals are private, and it doesn't give us a
# consistent way of testing whether a cell has a custom display_value
# or not. Prior to Pandas 1.4, we could test whether a cell's
# `display_value` differed from its `value`, and only stick the
# `display_value` in the protobuf when that was the case. In 1.4, an
# unmodified Styler will contain `display_value` strings for all
# cells, regardless of whether any formatting has been applied to
# that cell, so we no longer have this ability.
#
# So we're only testing that a cell's `display_value` is not None.
# In Pandas 1.4, it seems that `display_value` is never None, so this
# is purely a defense against future Styler changes.
return cell.get("display_value") is not None
cell_selector_regex = re.compile(r"row(\d+)_col(\d+)")
header_selector_regex = re.compile(r"level(\d+)_row(\d+)")
display_values = {}
for row in translated_style["body"]:
# row is a List[Dict], containing format data for each cell in the row,
# plus an extra first entry for the row header, which we skip
found_row_header = False
for cell in row:
cell_id = cell["id"] # a string in the form 'row0_col0'
if header_selector_regex.match(cell_id):
if not found_row_header:
# We don't care about processing row headers, but as
# a sanity check, ensure we only see one per row
found_row_header = True
continue
else:
raise RuntimeError('Found unexpected row header "%s"' % cell)
match = cell_selector_regex.match(cell_id)
if not match:
raise RuntimeError('Failed to parse cell selector "%s"' % cell_id)
if has_custom_display_value(cell):
row = int(match.group(1))
col = int(match.group(2))
display_values[(row, col)] = str(cell["display_value"])
return display_values
def _marshall_index(pandas_index, proto_index):
"""Convert an pandas.Index into a proto.Index.
pandas_index - Panda.Index or related (input)
proto_index - proto.Index (output)
"""
import pandas as pd
import numpy as np
if type(pandas_index) == pd.Index:
_marshall_any_array(np.array(pandas_index), proto_index.plain_index.data)
elif type(pandas_index) == pd.RangeIndex:
min = pandas_index.min()
max = pandas_index.max()
if pd.isna(min) or pd.isna(max):
proto_index.range_index.start = 0
proto_index.range_index.stop = 0
else:
proto_index.range_index.start = min
proto_index.range_index.stop = max + 1
elif type(pandas_index) == pd.MultiIndex:
for level in pandas_index.levels:
_marshall_index(level, proto_index.multi_index.levels.add())
if hasattr(pandas_index, "codes"):
index_codes = pandas_index.codes
else:
# Deprecated in Pandas 0.24, do don't bother covering.
index_codes = pandas_index.labels # pragma: no cover
for label in index_codes:
proto_index.multi_index.labels.add().data.extend(label)
elif type(pandas_index) == pd.DatetimeIndex:
if pandas_index.tz is None:
current_zone = tzlocal.get_localzone()
pandas_index = pandas_index.tz_localize(current_zone)
proto_index.datetime_index.data.data.extend(
pandas_index.map(datetime.datetime.isoformat)
)
elif type(pandas_index) == pd.TimedeltaIndex:
proto_index.timedelta_index.data.data.extend(pandas_index.astype(np.int64))
elif type(pandas_index) == pd.Int64Index:
proto_index.int_64_index.data.data.extend(pandas_index)
elif type(pandas_index) == pd.Float64Index:
proto_index.float_64_index.data.data.extend(pandas_index)
else:
raise NotImplementedError("Can't handle %s yet." % type(pandas_index))
def _marshall_table(pandas_table, proto_table):
"""Convert a sequence of 1D arrays into proto.Table.
pandas_table - Sequence of 1D arrays which are AnyArray compatible (input).
proto_table - proto.Table (output)
"""
for pandas_array in pandas_table:
if len(pandas_array) == 0:
continue
_marshall_any_array(pandas_array, proto_table.cols.add())
def _marshall_any_array(pandas_array, proto_array):
"""Convert a 1D numpy.Array into a proto.AnyArray.
pandas_array - 1D arrays which is AnyArray compatible (input).
proto_array - proto.AnyArray (output)
"""
import numpy as np
# Convert to np.array as necessary.
if not hasattr(pandas_array, "dtype"):
pandas_array = np.array(pandas_array)
# Only works on 1D arrays.
if len(pandas_array.shape) != 1:
raise ValueError("Array must be 1D.")
# Perform type-conversion based on the array dtype.
if issubclass(pandas_array.dtype.type, np.floating):
proto_array.doubles.data.extend(pandas_array)
elif issubclass(pandas_array.dtype.type, np.timedelta64):
proto_array.timedeltas.data.extend(pandas_array.astype(np.int64))
elif issubclass(pandas_array.dtype.type, np.integer):
proto_array.int64s.data.extend(pandas_array)
elif pandas_array.dtype == np.bool_:
proto_array.int64s.data.extend(pandas_array)
elif pandas_array.dtype == np.object_:
proto_array.strings.data.extend(map(str, pandas_array))
# dtype='string', <class 'pandas.core.arrays.string_.StringDtype'>
# NOTE: StringDtype is considered experimental.
# The implementation and parts of the API may change without warning.
elif pandas_array.dtype.name == "string":
proto_array.strings.data.extend(map(str, pandas_array))
# Setting a timezone changes (dtype, dtype.type) from
# 'datetime64[ns]', <class 'numpy.datetime64'>
# to
# datetime64[ns, UTC], <class 'pandas._libs.tslibs.timestamps.Timestamp'>
elif pandas_array.dtype.name.startswith("datetime64"):
# Just convert straight to ISO 8601, preserving timezone
# awareness/unawareness. The frontend will render it correctly.
proto_array.datetimes.data.extend(pandas_array.map(datetime.datetime.isoformat))
else:
raise NotImplementedError("Dtype %s not understood." % pandas_array.dtype)

View File

@@ -0,0 +1,199 @@
# Copyright 2018-2022 Streamlit Inc.
#
# 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.
"""A Python wrapper around Vega-Lite."""
import json
from typing import cast
import streamlit
import streamlit.elements.legacy_data_frame as data_frame
import streamlit.elements.lib.dicttools as dicttools
from streamlit.logger import get_logger
from streamlit.proto.VegaLiteChart_pb2 import VegaLiteChart as VegaLiteChartProto
LOGGER = get_logger(__name__)
class LegacyVegaLiteMixin:
def _legacy_vega_lite_chart(
self,
data=None,
spec=None,
use_container_width=False,
**kwargs,
):
"""Display a chart using the Vega-Lite library.
Parameters
----------
data : pandas.DataFrame, pandas.Styler, numpy.ndarray, Iterable, dict,
or None
Either the data to be plotted or a Vega-Lite spec containing the
data (which more closely follows the Vega-Lite API).
spec : dict or None
The Vega-Lite spec for the chart. If the spec was already passed in
the previous argument, this must be set to None. See
https://vega.github.io/vega-lite/docs/ for more info.
use_container_width : bool
If True, set the chart width to the column width. This takes
precedence over Vega-Lite's native `width` value.
**kwargs : any
Same as spec, but as keywords.
Example
-------
>>> import pandas as pd
>>> import numpy as np
>>>
>>> df = pd.DataFrame(
... np.random.randn(200, 3),
... columns=['a', 'b', 'c'])
>>>
>>> st._legacy_vega_lite_chart(df, {
... 'mark': {'type': 'circle', 'tooltip': True},
... 'encoding': {
... 'x': {'field': 'a', 'type': 'quantitative'},
... 'y': {'field': 'b', 'type': 'quantitative'},
... 'size': {'field': 'c', 'type': 'quantitative'},
... 'color': {'field': 'c', 'type': 'quantitative'},
... },
... })
.. output::
https://static.streamlit.io/0.25.0-2JkNY/index.html?id=8jmmXR8iKoZGV4kXaKGYV5
height: 200px
Examples of Vega-Lite usage without Streamlit can be found at
https://vega.github.io/vega-lite/examples/. Most of those can be easily
translated to the syntax shown above.
"""
vega_lite_chart_proto = VegaLiteChartProto()
marshall(
vega_lite_chart_proto,
data,
spec,
use_container_width=use_container_width,
**kwargs,
)
return self.dg._enqueue("vega_lite_chart", vega_lite_chart_proto)
@property
def dg(self) -> "streamlit.delta_generator.DeltaGenerator":
"""Get our DeltaGenerator."""
return cast("streamlit.delta_generator.DeltaGenerator", self)
def marshall(proto, data=None, spec=None, use_container_width=False, **kwargs):
"""Construct a Vega-Lite chart object.
See DeltaGenerator._legacy_vega_lite_chart for docs.
"""
# Support passing data inside spec['datasets'] and spec['data'].
# (The data gets pulled out of the spec dict later on.)
if isinstance(data, dict) and spec is None:
spec = data
data = None
# Support passing no spec arg, but filling it with kwargs.
# Example:
# marshall(proto, baz='boz')
if spec is None:
spec = dict()
else:
# Clone the spec dict, since we may be mutating it.
spec = dict(spec)
# Support passing in kwargs. Example:
# marshall(proto, {foo: 'bar'}, baz='boz')
if len(kwargs):
# Merge spec with unflattened kwargs, where kwargs take precedence.
# This only works for string keys, but kwarg keys are strings anyways.
spec = dict(spec, **dicttools.unflatten(kwargs, _CHANNELS))
if len(spec) == 0:
raise ValueError("Vega-Lite charts require a non-empty spec dict.")
if "autosize" not in spec:
spec["autosize"] = {"type": "fit", "contains": "padding"}
# Pull data out of spec dict when it's in a 'dataset' key:
# marshall(proto, {datasets: {foo: df1, bar: df2}, ...})
if "datasets" in spec:
for k, v in spec["datasets"].items():
dataset = proto.datasets.add()
dataset.name = str(k)
dataset.has_name = True
data_frame.marshall_data_frame(v, dataset.data)
del spec["datasets"]
# Pull data out of spec dict when it's in a top-level 'data' key:
# marshall(proto, {data: df})
# marshall(proto, {data: {values: df, ...}})
# marshall(proto, {data: {url: 'url'}})
# marshall(proto, {data: {name: 'foo'}})
if "data" in spec:
data_spec = spec["data"]
if isinstance(data_spec, dict):
if "values" in data_spec:
data = data_spec["values"]
del spec["data"]
else:
data = data_spec
del spec["data"]
proto.spec = json.dumps(spec)
proto.use_container_width = use_container_width
if data is not None:
data_frame.marshall_data_frame(data, proto.data)
# See https://vega.github.io/vega-lite/docs/encoding.html
_CHANNELS = set(
[
"x",
"y",
"x2",
"y2",
"xError",
"yError2",
"xError",
"yError2",
"longitude",
"latitude",
"color",
"opacity",
"fillOpacity",
"strokeOpacity",
"strokeWidth",
"size",
"shape",
"text",
"tooltip",
"href",
"key",
"order",
"detail",
"facet",
"row",
"column",
]
)

View File

@@ -0,0 +1,13 @@
# Copyright 2018-2022 Streamlit Inc.
#
# 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.

View File

@@ -0,0 +1,135 @@
# Copyright 2018-2022 Streamlit Inc.
#
# 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.
"""Tools for working with dicts."""
from typing import Any, Dict, Optional
def _unflatten_single_dict(flat_dict):
"""Convert a flat dict of key-value pairs to dict tree.
Example
-------
_unflatten_single_dict({
foo_bar_baz: 123,
foo_bar_biz: 456,
x_bonks: 'hi',
})
# Returns:
# {
# foo: {
# bar: {
# baz: 123,
# biz: 456,
# },
# },
# x: {
# bonks: 'hi'
# }
# }
Parameters
----------
flat_dict : dict
A one-level dict where keys are fully-qualified paths separated by
underscores.
Returns
-------
dict
A tree made of dicts inside of dicts.
"""
out = dict() # type: Dict[str, Any]
for pathstr, v in flat_dict.items():
path = pathstr.split("_")
prev_dict = None # type: Optional[Dict[str, Any]]
curr_dict = out
for k in path:
if k not in curr_dict:
curr_dict[k] = dict()
prev_dict = curr_dict
curr_dict = curr_dict[k]
if prev_dict is not None:
prev_dict[k] = v
return out
def unflatten(flat_dict, encodings=None):
"""Converts a flat dict of key-value pairs to a spec tree.
Example:
unflatten({
foo_bar_baz: 123,
foo_bar_biz: 456,
x_bonks: 'hi',
}, ['x'])
# Returns:
# {
# foo: {
# bar: {
# baz: 123,
# biz: 456,
# },
# },
# encoding: { # This gets added automatically
# x: {
# bonks: 'hi'
# }
# }
# }
Args:
-----
flat_dict: dict
A flat dict where keys are fully-qualified paths separated by
underscores.
encodings: set
Key names that should be automatically moved into the 'encoding' key.
Returns:
--------
A tree made of dicts inside of dicts.
"""
if encodings is None:
encodings = set()
out_dict = _unflatten_single_dict(flat_dict)
for k, v in list(out_dict.items()):
# Unflatten child dicts:
if isinstance(v, dict):
v = unflatten(v, encodings)
elif hasattr(v, "__iter__"):
for i, child in enumerate(v):
if isinstance(child, dict):
v[i] = unflatten(child, encodings)
# Move items into 'encoding' if needed:
if k in encodings:
if "encoding" not in out_dict:
out_dict["encoding"] = dict()
out_dict["encoding"][k] = v
out_dict.pop(k)
return out_dict

View File

@@ -0,0 +1,210 @@
# Copyright 2018-2022 Streamlit Inc.
#
# 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.
"""A wrapper for simple PyDeck scatter charts."""
import copy
import json
from typing import Any, Dict
from typing import cast
import pandas as pd
import streamlit
import streamlit.elements.deck_gl_json_chart as deck_gl_json_chart
from streamlit.errors import StreamlitAPIException
from streamlit.proto.DeckGlJsonChart_pb2 import DeckGlJsonChart as DeckGlJsonChartProto
class MapMixin:
def map(self, data=None, zoom=None, use_container_width=True):
"""Display a map with points on it.
This is a wrapper around st.pydeck_chart to quickly create scatterplot
charts on top of a map, with auto-centering and auto-zoom.
When using this command, we advise all users to use a personal Mapbox
token. This ensures the map tiles used in this chart are more
robust. You can do this with the mapbox.token config option.
To get a token for yourself, create an account at
https://mapbox.com. It's free! (for moderate usage levels). For more
info on how to set config options, see
https://docs.streamlit.io/library/advanced-features/configuration#set-configuration-options
Parameters
----------
data : pandas.DataFrame, pandas.Styler, numpy.ndarray, Iterable, dict,
or None
The data to be plotted. Must have columns called 'lat', 'lon',
'latitude', or 'longitude'.
zoom : int
Zoom level as specified in
https://wiki.openstreetmap.org/wiki/Zoom_levels
Example
-------
>>> import streamlit as st
>>> import pandas as pd
>>> import numpy as np
>>>
>>> df = pd.DataFrame(
... np.random.randn(1000, 2) / [50, 50] + [37.76, -122.4],
... columns=['lat', 'lon'])
>>>
>>> st.map(df)
.. output::
https://share.streamlit.io/streamlit/docs/main/python/api-examples-source/charts.map.py
height: 650px
"""
map_proto = DeckGlJsonChartProto()
map_proto.json = to_deckgl_json(data, zoom)
map_proto.use_container_width = use_container_width
return self.dg._enqueue("deck_gl_json_chart", map_proto)
@property
def dg(self) -> "streamlit.delta_generator.DeltaGenerator":
"""Get our DeltaGenerator."""
return cast("streamlit.delta_generator.DeltaGenerator", self)
# Map used as the basis for st.map.
_DEFAULT_MAP = dict(deck_gl_json_chart.EMPTY_MAP) # type: Dict[str, Any]
_DEFAULT_MAP["mapStyle"] = "mapbox://styles/mapbox/light-v10"
# Other default parameters for st.map.
_DEFAULT_COLOR = [200, 30, 0, 160]
_DEFAULT_ZOOM_LEVEL = 12
_ZOOM_LEVELS = [
360,
180,
90,
45,
22.5,
11.25,
5.625,
2.813,
1.406,
0.703,
0.352,
0.176,
0.088,
0.044,
0.022,
0.011,
0.005,
0.003,
0.001,
0.0005,
0.00025,
]
def _get_zoom_level(distance):
"""Get the zoom level for a given distance in degrees.
See https://wiki.openstreetmap.org/wiki/Zoom_levels for reference.
Parameters
----------
distance : float
How many degrees of longitude should fit in the map.
Returns
-------
int
The zoom level, from 0 to 20.
"""
# For small number of points the default zoom level will be used.
if distance < _ZOOM_LEVELS[-1]:
return _DEFAULT_ZOOM_LEVEL
for i in range(len(_ZOOM_LEVELS) - 1):
if _ZOOM_LEVELS[i + 1] < distance <= _ZOOM_LEVELS[i]:
return i
def to_deckgl_json(data, zoom):
if data is None or data.empty:
return json.dumps(_DEFAULT_MAP)
if "lat" in data:
lat = "lat"
elif "latitude" in data:
lat = "latitude"
else:
raise StreamlitAPIException(
'Map data must contain a column named "latitude" or "lat".'
)
if "lon" in data:
lon = "lon"
elif "longitude" in data:
lon = "longitude"
else:
raise StreamlitAPIException(
'Map data must contain a column called "longitude" or "lon".'
)
if data[lon].isnull().values.any() or data[lat].isnull().values.any():
raise StreamlitAPIException("Latitude and longitude data must be numeric.")
data = pd.DataFrame(data)
min_lat = data[lat].min()
max_lat = data[lat].max()
min_lon = data[lon].min()
max_lon = data[lon].max()
center_lat = (max_lat + min_lat) / 2.0
center_lon = (max_lon + min_lon) / 2.0
range_lon = abs(max_lon - min_lon)
range_lat = abs(max_lat - min_lat)
if zoom == None:
if range_lon > range_lat:
longitude_distance = range_lon
else:
longitude_distance = range_lat
zoom = _get_zoom_level(longitude_distance)
# "+1" because itertuples includes the row index.
lon_col_index = data.columns.get_loc(lon) + 1
lat_col_index = data.columns.get_loc(lat) + 1
final_data = []
for row in data.itertuples():
final_data.append(
{"lon": float(row[lon_col_index]), "lat": float(row[lat_col_index])}
)
default = copy.deepcopy(_DEFAULT_MAP)
default["initialViewState"]["latitude"] = center_lat
default["initialViewState"]["longitude"] = center_lon
default["initialViewState"]["zoom"] = zoom
default["layers"] = [
{
"@@type": "ScatterplotLayer",
"getPosition": "@@=[lon, lat]",
"getRadius": 10,
"radiusScale": 10,
"radiusMinPixels": 3,
"getFillColor": _DEFAULT_COLOR,
"data": final_data,
}
]
return json.dumps(default)

View File

@@ -0,0 +1,265 @@
# Copyright 2018-2022 Streamlit Inc.
#
# 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 typing import cast
import streamlit
from streamlit import type_util
from streamlit.proto.Markdown_pb2 import Markdown as MarkdownProto
from .utils import clean_text
class MarkdownMixin:
def markdown(self, body, unsafe_allow_html=False):
"""Display string formatted as Markdown.
Parameters
----------
body : str
The string to display as Github-flavored Markdown. Syntax
information can be found at: https://github.github.com/gfm.
This also supports:
* Emoji shortcodes, such as `:+1:` and `:sunglasses:`.
For a list of all supported codes,
see https://share.streamlit.io/streamlit/emoji-shortcodes.
* LaTeX expressions, by wrapping them in "$" or "$$" (the "$$"
must be on their own lines). Supported LaTeX functions are listed
at https://katex.org/docs/supported.html.
unsafe_allow_html : bool
By default, any HTML tags found in the body will be escaped and
therefore treated as pure text. This behavior may be turned off by
setting this argument to True.
That said, we *strongly advise against it*. It is hard to write
secure HTML, so by using this argument you may be compromising your
users' security. For more information, see:
https://github.com/streamlit/streamlit/issues/152
*Also note that `unsafe_allow_html` is a temporary measure and may
be removed from Streamlit at any time.*
If you decide to turn on HTML anyway, we ask you to please tell us
your exact use case here:
https://discuss.streamlit.io/t/96
This will help us come up with safe APIs that allow you to do what
you want.
Example
-------
>>> st.markdown('Streamlit is **_really_ cool**.')
"""
markdown_proto = MarkdownProto()
markdown_proto.body = clean_text(body)
markdown_proto.allow_html = unsafe_allow_html
return self.dg._enqueue("markdown", markdown_proto)
def header(self, body, anchor=None):
"""Display text in header formatting.
Parameters
----------
body : str
The text to display.
anchor : str
The anchor name of the header that can be accessed with #anchor
in the URL. If omitted, it generates an anchor using the body.
Example
-------
>>> st.header('This is a header')
"""
header_proto = MarkdownProto()
if anchor is None:
header_proto.body = f"## {clean_text(body)}"
else:
header_proto.body = f'<h2 data-anchor="{anchor}">{clean_text(body)}</h2>'
header_proto.allow_html = True
return self.dg._enqueue("markdown", header_proto)
def subheader(self, body, anchor=None):
"""Display text in subheader formatting.
Parameters
----------
body : str
The text to display.
anchor : str
The anchor name of the header that can be accessed with #anchor
in the URL. If omitted, it generates an anchor using the body.
Example
-------
>>> st.subheader('This is a subheader')
"""
subheader_proto = MarkdownProto()
if anchor is None:
subheader_proto.body = f"### {clean_text(body)}"
else:
subheader_proto.body = f'<h3 data-anchor="{anchor}">{clean_text(body)}</h3>'
subheader_proto.allow_html = True
return self.dg._enqueue("markdown", subheader_proto)
def code(self, body, language="python"):
"""Display a code block with optional syntax highlighting.
(This is a convenience wrapper around `st.markdown()`)
Parameters
----------
body : str
The string to display as code.
language : str
The language that the code is written in, for syntax highlighting.
If omitted, the code will be unstyled.
Example
-------
>>> code = '''def hello():
... print("Hello, Streamlit!")'''
>>> st.code(code, language='python')
"""
code_proto = MarkdownProto()
markdown = "```%(language)s\n%(body)s\n```" % {
"language": language or "",
"body": body,
}
code_proto.body = clean_text(markdown)
return self.dg._enqueue("markdown", code_proto)
def title(self, body, anchor=None):
"""Display text in title formatting.
Each document should have a single `st.title()`, although this is not
enforced.
Parameters
----------
body : str
The text to display.
anchor : str
The anchor name of the header that can be accessed with #anchor
in the URL. If omitted, it generates an anchor using the body.
Example
-------
>>> st.title('This is a title')
"""
title_proto = MarkdownProto()
if anchor is None:
title_proto.body = f"# {clean_text(body)}"
else:
title_proto.body = f'<h1 data-anchor="{anchor}">{clean_text(body)}</h1>'
title_proto.allow_html = True
return self.dg._enqueue("markdown", title_proto)
def caption(self, body, unsafe_allow_html=False):
"""Display text in small font.
This should be used for captions, asides, footnotes, sidenotes, and
other explanatory text.
Parameters
----------
body : str
The text to display.
unsafe_allow_html : bool
By default, any HTML tags found in strings will be escaped and
therefore treated as pure text. This behavior may be turned off by
setting this argument to True.
That said, *we strongly advise against it*. It is hard to write secure
HTML, so by using this argument you may be compromising your users'
security. For more information, see:
https://github.com/streamlit/streamlit/issues/152
**Also note that `unsafe_allow_html` is a temporary measure and may be
removed from Streamlit at any time.**
If you decide to turn on HTML anyway, we ask you to please tell us your
exact use case here:
https://discuss.streamlit.io/t/96 .
This will help us come up with safe APIs that allow you to do what you
want.
Example
-------
>>> st.caption('This is a string that explains something above.')
"""
caption_proto = MarkdownProto()
caption_proto.body = clean_text(body)
caption_proto.allow_html = unsafe_allow_html
caption_proto.is_caption = True
return self.dg._enqueue("markdown", caption_proto)
def latex(self, body):
# This docstring needs to be "raw" because of the backslashes in the
# example below.
r"""Display mathematical expressions formatted as LaTeX.
Supported LaTeX functions are listed at
https://katex.org/docs/supported.html.
Parameters
----------
body : str or SymPy expression
The string or SymPy expression to display as LaTeX. If str, it's
a good idea to use raw Python strings since LaTeX uses backslashes
a lot.
Example
-------
>>> st.latex(r'''
... a + ar + a r^2 + a r^3 + \cdots + a r^{n-1} =
... \sum_{k=0}^{n-1} ar^k =
... a \left(\frac{1-r^{n}}{1-r}\right)
... ''')
"""
if type_util.is_sympy_expession(body):
import sympy
body = sympy.latex(body)
latex_proto = MarkdownProto()
latex_proto.body = "$$\n%s\n$$" % clean_text(body)
return self.dg._enqueue("markdown", latex_proto)
@property
def dg(self) -> "streamlit.delta_generator.DeltaGenerator":
"""Get our DeltaGenerator."""
return cast("streamlit.delta_generator.DeltaGenerator", self)

View File

@@ -0,0 +1,246 @@
# Copyright 2018-2022 Streamlit Inc.
#
# 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.
import io
import re
from typing import cast
from validators import url
import streamlit
from streamlit import type_util
from streamlit.in_memory_file_manager import in_memory_file_manager
from streamlit.proto.Audio_pb2 import Audio as AudioProto
from streamlit.proto.Video_pb2 import Video as VideoProto
class MediaMixin:
def audio(self, data, format="audio/wav", start_time=0):
"""Display an audio player.
Parameters
----------
data : str, bytes, BytesIO, numpy.ndarray, or file opened with
io.open().
Raw audio data, filename, or a URL pointing to the file to load.
Numpy arrays and raw data formats must include all necessary file
headers to match specified file format.
start_time: int
The time from which this element should start playing.
format : str
The mime type for the audio file. Defaults to 'audio/wav'.
See https://tools.ietf.org/html/rfc4281 for more info.
Example
-------
>>> audio_file = open('myaudio.ogg', 'rb')
>>> audio_bytes = audio_file.read()
>>>
>>> st.audio(audio_bytes, format='audio/ogg')
.. output::
https://share.streamlit.io/streamlit/docs/main/python/api-examples-source/charts.audio.py
height: 465px
"""
audio_proto = AudioProto()
coordinates = self.dg._get_delta_path_str()
marshall_audio(coordinates, audio_proto, data, format, start_time)
return self.dg._enqueue("audio", audio_proto)
def video(self, data, format="video/mp4", start_time=0):
"""Display a video player.
Parameters
----------
data : str, bytes, BytesIO, numpy.ndarray, or file opened with
io.open().
Raw video data, filename, or URL pointing to a video to load.
Includes support for YouTube URLs.
Numpy arrays and raw data formats must include all necessary file
headers to match specified file format.
format : str
The mime type for the video file. Defaults to 'video/mp4'.
See https://tools.ietf.org/html/rfc4281 for more info.
start_time: int
The time from which this element should start playing.
Example
-------
>>> video_file = open('myvideo.mp4', 'rb')
>>> video_bytes = video_file.read()
>>>
>>> st.video(video_bytes)
.. output::
https://share.streamlit.io/streamlit/docs/main/python/api-examples-source/charts.video.py
height: 700px
.. note::
Some videos may not display if they are encoded using MP4V (which is an export option in OpenCV), as this codec is
not widely supported by browsers. Converting your video to H.264 will allow the video to be displayed in Streamlit.
See this `StackOverflow post <https://stackoverflow.com/a/49535220/2394542>`_ or this
`Streamlit forum post <https://discuss.streamlit.io/t/st-video-doesnt-show-opencv-generated-mp4/3193/2>`_
for more information.
"""
video_proto = VideoProto()
coordinates = self.dg._get_delta_path_str()
marshall_video(coordinates, video_proto, data, format, start_time)
return self.dg._enqueue("video", video_proto)
@property
def dg(self) -> "streamlit.delta_generator.DeltaGenerator":
"""Get our DeltaGenerator."""
return cast("streamlit.delta_generator.DeltaGenerator", self)
# Regular expression explained at https://regexr.com/4n2l2 Covers any youtube
# URL (incl. shortlinks and embed links) and extracts its code.
YOUTUBE_RE = re.compile(
# Protocol
r"http(?:s?):\/\/"
# Domain
r"(?:www\.)?youtu(?:be\.com|\.be)\/"
# Path and query string
r"(?P<watch>(watch\?v=)|embed\/)?(?P<code>[\w\-\_]*)(&(amp;)?[\w\?=]*)?"
)
def _reshape_youtube_url(url):
"""Return whether URL is any kind of YouTube embed or watch link. If so,
reshape URL into an embed link suitable for use in an iframe.
If not a YouTube URL, return None.
Parameters
----------
url : str or bytes
Example
-------
>>> print(_reshape_youtube_url('https://youtu.be/_T8LGqJtuGc'))
.. output::
https://www.youtube.com/embed/_T8LGqJtuGc
"""
match = YOUTUBE_RE.match(url)
if match:
return "https://www.youtube.com/embed/{code}".format(**match.groupdict())
return None
def _marshall_av_media(coordinates, proto, data, mimetype):
"""Fill audio or video proto based on contents of data.
Given a string, check if it's a url; if so, send it out without modification.
Otherwise assume strings are filenames and let any OS errors raise.
Load data either from file or through bytes-processing methods into a
InMemoryFile object. Pack proto with generated Tornado-based URL.
"""
# Audio and Video methods have already checked if this is a URL by this point.
if isinstance(data, str):
# Assume it's a filename or blank. Allow OS-based file errors.
with open(data, "rb") as fh:
this_file = in_memory_file_manager.add(fh.read(), mimetype, coordinates)
proto.url = this_file.url
return
if data is None:
# Allow empty values so media players can be shown without media.
return
# Assume bytes; try methods until we run out.
if isinstance(data, bytes):
pass
elif isinstance(data, io.BytesIO):
data.seek(0)
data = data.getvalue()
elif isinstance(data, io.RawIOBase) or isinstance(data, io.BufferedReader):
data.seek(0)
data = data.read()
elif type_util.is_type(data, "numpy.ndarray"):
data = data.tobytes()
else:
raise RuntimeError("Invalid binary data format: %s" % type(data))
this_file = in_memory_file_manager.add(data, mimetype, coordinates)
proto.url = this_file.url
def marshall_video(coordinates, proto, data, mimetype="video/mp4", start_time=0):
"""Marshalls a video proto, using url processors as needed.
Parameters
----------
coordinates : str
proto : the proto to fill. Must have a string field called "data".
data : str, bytes, BytesIO, numpy.ndarray, or file opened with
io.open().
Raw video data or a string with a URL pointing to the video
to load. Includes support for YouTube URLs.
If passing the raw data, this must include headers and any other
bytes required in the actual file.
mimetype : str
The mime type for the video file. Defaults to 'video/mp4'.
See https://tools.ietf.org/html/rfc4281 for more info.
start_time : int
The time from which this element should start playing. (default: 0)
"""
proto.start_time = start_time
# "type" distinguishes between YouTube and non-YouTube links
proto.type = VideoProto.Type.NATIVE
if isinstance(data, str) and url(data):
youtube_url = _reshape_youtube_url(data)
if youtube_url:
proto.url = youtube_url
proto.type = VideoProto.Type.YOUTUBE_IFRAME
else:
proto.url = data
else:
_marshall_av_media(coordinates, proto, data, mimetype)
def marshall_audio(coordinates, proto, data, mimetype="audio/wav", start_time=0):
"""Marshalls an audio proto, using data and url processors as needed.
Parameters
----------
coordinates : str
proto : The proto to fill. Must have a string field called "url".
data : str, bytes, BytesIO, numpy.ndarray, or file opened with
io.open()
Raw audio data or a string with a URL pointing to the file to load.
If passing the raw data, this must include headers and any other bytes
required in the actual file.
mimetype : str
The mime type for the audio file. Defaults to "audio/wav".
See https://tools.ietf.org/html/rfc4281 for more info.
start_time : int
The time from which this element should start playing. (default: 0)
"""
proto.start_time = start_time
if isinstance(data, str) and url(data):
proto.url = data
else:
_marshall_av_media(coordinates, proto, data, mimetype)

View File

@@ -0,0 +1,184 @@
# Copyright 2018-2022 Streamlit Inc.
#
# 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 textwrap import dedent
from typing import Optional, cast
import attr
import streamlit
from streamlit.errors import StreamlitAPIException
from streamlit.proto.Metric_pb2 import Metric as MetricProto
from .utils import clean_text
@attr.s(auto_attribs=True, slots=True)
class MetricColorAndDirection:
color: Optional[int]
direction: Optional[int]
class MetricMixin:
def metric(self, label, value, delta=None, delta_color="normal"):
"""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 <https://github.com/azaitsev/millify>`_
or `numerize <https://github.com/davidsa03/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
value : int, float, str, or None
Value of the metric. None is rendered as a long dash.
delta : int, float, 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 : str
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.
Example
-------
>>> st.metric(label="Temperature", value="70 °F", delta="1.2 °F")
.. output::
https://share.streamlit.io/streamlit/docs/main/python/api-examples-source/metric.example1.py
height: 210px
``st.metric`` looks especially nice in combination with ``st.columns``:
>>> 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://share.streamlit.io/streamlit/docs/main/python/api-examples-source/metric.example2.py
height: 210px
The delta indicator color can also be inverted or turned off:
>>> 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://share.streamlit.io/streamlit/docs/main/python/api-examples-source/metric.example3.py
height: 320px
"""
metric_proto = MetricProto()
metric_proto.body = self.parse_value(value)
metric_proto.label = self.parse_label(label)
metric_proto.delta = self.parse_delta(delta)
color_and_direction = self.determine_delta_color_and_direction(
clean_text(delta_color), delta
)
metric_proto.color = color_and_direction.color
metric_proto.direction = color_and_direction.direction
return str(self.dg._enqueue("metric", metric_proto))
def parse_label(self, label):
if not isinstance(label, str):
raise TypeError(
f"'{str(label)}' is of type {str(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(self, value):
if value is None:
return ""
if isinstance(value, float) or isinstance(value, int) or isinstance(value, str):
return str(value)
elif hasattr(value, "item"):
# Add support for numpy values (e.g. int16, float64, etc.)
try:
# Item could also be just a variable, so we use try, except
if isinstance(value.item(), float) or isinstance(value.item(), int):
return str(value.item())
except Exception:
pass
raise TypeError(
f"'{str(value)}' is of type {str(type(value))}, which is not an accepted type."
" value only accepts: int, float, str, or None."
" Please convert the value to an accepted type."
)
def parse_delta(self, delta):
if delta is None or delta == "":
return ""
if isinstance(delta, str):
return dedent(delta)
elif isinstance(delta, int) or isinstance(delta, float):
return str(delta)
else:
raise TypeError(
f"'{str(delta)}' is of type {str(type(delta))}, which is not an accepted type."
" delta only accepts: int, float, str, or None."
" Please convert the value to an accepted type."
)
def determine_delta_color_and_direction(self, delta_color, delta):
cd = MetricColorAndDirection(color=None, direction=None)
if delta is None or delta == "":
cd.color = MetricProto.MetricColor.GRAY
cd.direction = MetricProto.MetricDirection.NONE
return cd
if self.is_negative(delta):
if delta_color == "normal":
cd.color = MetricProto.MetricColor.RED
elif delta_color == "inverse":
cd.color = MetricProto.MetricColor.GREEN
elif delta_color == "off":
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
elif delta_color == "off":
cd.color = MetricProto.MetricColor.GRAY
cd.direction = MetricProto.MetricDirection.UP
if cd.color is None or cd.direction is None:
raise StreamlitAPIException(
f"'{str(delta_color)}' is not an accepted value. delta_color only accepts: "
"'normal', 'inverse', or 'off'"
)
return cd
def is_negative(self, delta):
return dedent(str(delta)).startswith("-")
@property
def dg(self) -> "streamlit.delta_generator.DeltaGenerator":
return cast("streamlit.delta_generator.DeltaGenerator", self)

View File

@@ -0,0 +1,218 @@
# Copyright 2018-2022 Streamlit Inc.
#
# 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 textwrap import dedent
from typing import Any, Callable, Optional, cast, List
import streamlit
from streamlit.errors import StreamlitAPIException
from streamlit.proto.MultiSelect_pb2 import MultiSelect as MultiSelectProto
from streamlit.scriptrunner import ScriptRunContext, get_script_run_ctx
from streamlit.type_util import Key, OptionSequence, ensure_indexable, is_type, to_key
from streamlit.state import (
register_widget,
WidgetArgs,
WidgetCallback,
WidgetKwargs,
)
from .form import current_form_id
from .utils import check_callback_rules, check_session_state_rules
class MultiSelectMixin:
def multiselect(
self,
label: str,
options: OptionSequence,
default: Optional[Any] = None,
format_func: Callable[[Any], Any] = str,
key: Optional[Key] = None,
help: Optional[str] = None,
on_change: Optional[WidgetCallback] = None,
args: Optional[WidgetArgs] = None,
kwargs: Optional[WidgetKwargs] = None,
*, # keyword-only arguments:
disabled: bool = False,
) -> List[Any]:
"""Display a multiselect widget.
The multiselect widget starts as empty.
Parameters
----------
label : str
A short label explaining to the user what this select widget is for.
options : Sequence[V], numpy.ndarray, pandas.Series, pandas.DataFrame, or pandas.Index
Labels for the select options. This will be cast to str internally
by default. For pandas.DataFrame, the first column is selected.
default: [V], V, or None
List of default values. Can also be a single value.
format_func : function
Function to modify the display of selectbox options. It receives
the raw option as an argument and should output the label to be
shown for that option. This has no impact on the return value of
the multiselect.
key : str or int
An optional string or integer to use as the unique key for the widget.
If this is omitted, a key will be generated for the widget
based on its content. Multiple widgets of the same type may
not share the same key.
help : str
An optional tooltip that gets displayed next to the multiselect.
on_change : callable
An optional callback invoked when this multiselect's value changes.
args : tuple
An optional tuple of args to pass to the callback.
kwargs : dict
An optional dict of kwargs to pass to the callback.
disabled : bool
An optional boolean, which disables the multiselect widget if set
to True. The default is False. This argument can only be supplied
by keyword.
Returns
-------
list
A list with the selected options
Example
-------
>>> options = st.multiselect(
... 'What are your favorite colors',
... ['Green', 'Yellow', 'Red', 'Blue'],
... ['Yellow', 'Red'])
>>>
>>> st.write('You selected:', options)
.. output::
https://share.streamlit.io/streamlit/docs/main/python/api-examples-source/widget.multiselect.py
height: 420px
.. note::
User experience can be degraded for large lists of `options` (100+), as this widget
is not designed to handle arbitrary text search efficiently. See this
`thread <https://discuss.streamlit.io/t/streamlit-loading-column-data-takes-too-much-time/1791>`_
on the Streamlit community forum for more information and
`GitHub issue #1059 <https://github.com/streamlit/streamlit/issues/1059>`_ for updates on the issue.
"""
ctx = get_script_run_ctx()
return self._multiselect(
label=label,
options=options,
default=default,
format_func=format_func,
key=key,
help=help,
on_change=on_change,
args=args,
kwargs=kwargs,
disabled=disabled,
ctx=ctx,
)
def _multiselect(
self,
label: str,
options: OptionSequence,
default: Optional[Any] = None,
format_func: Callable[[Any], Any] = str,
key: Optional[Key] = None,
help: Optional[str] = None,
on_change: Optional[WidgetCallback] = None,
args: Optional[WidgetArgs] = None,
kwargs: Optional[WidgetKwargs] = None,
*, # keyword-only arguments:
disabled: bool = False,
ctx: Optional[ScriptRunContext] = None,
) -> List[Any]:
key = to_key(key)
check_callback_rules(self.dg, on_change)
check_session_state_rules(default_value=default, key=key)
opt = ensure_indexable(options)
# Perform validation checks and return indices base on the default values.
def _check_and_convert_to_indices(opt, default_values):
if default_values is None and None not in opt:
return None
if not isinstance(default_values, list):
# This if is done before others because calling if not x (done
# right below) when x is of type pd.Series() or np.array() throws a
# ValueError exception.
if is_type(default_values, "numpy.ndarray") or is_type(
default_values, "pandas.core.series.Series"
):
default_values = list(default_values)
elif not default_values or default_values in opt:
default_values = [default_values]
else:
default_values = list(default_values)
for value in default_values:
if value not in opt:
raise StreamlitAPIException(
"Every Multiselect default value must exist in options"
)
return [opt.index(value) for value in default_values]
indices = _check_and_convert_to_indices(opt, default)
multiselect_proto = MultiSelectProto()
multiselect_proto.label = label
default_value = [] if indices is None else indices
multiselect_proto.default[:] = default_value
multiselect_proto.options[:] = [str(format_func(option)) for option in opt]
multiselect_proto.form_id = current_form_id(self.dg)
if help is not None:
multiselect_proto.help = dedent(help)
def deserialize_multiselect(
ui_value: Optional[List[int]], widget_id: str = ""
) -> List[str]:
current_value = ui_value if ui_value is not None else default_value
return [opt[i] for i in current_value]
def serialize_multiselect(value):
return _check_and_convert_to_indices(opt, value)
current_value, set_frontend_value = register_widget(
"multiselect",
multiselect_proto,
user_key=key,
on_change_handler=on_change,
args=args,
kwargs=kwargs,
deserializer=deserialize_multiselect,
serializer=serialize_multiselect,
ctx=ctx,
)
# This needs to be done after register_widget because we don't want
# the following proto fields to affect a widget's ID.
multiselect_proto.disabled = disabled
if set_frontend_value:
multiselect_proto.value[:] = _check_and_convert_to_indices(
opt, current_value
)
multiselect_proto.set_value = True
self.dg._enqueue("multiselect", multiselect_proto)
return cast(List[str], current_value)
@property
def dg(self) -> "streamlit.delta_generator.DeltaGenerator":
"""Get our DeltaGenerator."""
return cast("streamlit.delta_generator.DeltaGenerator", self)

View File

@@ -0,0 +1,306 @@
# Copyright 2018-2022 Streamlit Inc.
#
# 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.
import numbers
from streamlit.scriptrunner import ScriptRunContext, get_script_run_ctx
from streamlit.type_util import Key, to_key
from textwrap import dedent
from typing import Optional, Union, cast
import streamlit
from streamlit.errors import StreamlitAPIException
from streamlit.js_number import JSNumber, JSNumberBoundsException
from streamlit.proto.NumberInput_pb2 import NumberInput as NumberInputProto
from streamlit.state import (
register_widget,
NoValue,
WidgetArgs,
WidgetCallback,
WidgetKwargs,
)
from .form import current_form_id
from .utils import check_callback_rules, check_session_state_rules
Number = Union[int, float]
class NumberInputMixin:
def number_input(
self,
label: str,
min_value: Optional[Number] = None,
max_value: Optional[Number] = None,
value: Union[NoValue, Number, None] = NoValue(),
step: Optional[Number] = None,
format: Optional[str] = None,
key: Optional[Key] = None,
help: Optional[str] = None,
on_change: Optional[WidgetCallback] = None,
args: Optional[WidgetArgs] = None,
kwargs: Optional[WidgetKwargs] = None,
*, # keyword-only arguments:
disabled: bool = False,
) -> Number:
"""Display a numeric input widget.
Parameters
----------
label : str
A short label explaining to the user what this input is for.
min_value : int or float or None
The minimum permitted value.
If None, there will be no minimum.
max_value : int or float or None
The maximum permitted value.
If None, there will be no maximum.
value : int or float or None
The value of this widget when it first renders.
Defaults to min_value, or 0.0 if min_value is None
step : int or float or None
The stepping interval.
Defaults to 1 if the value is an int, 0.01 otherwise.
If the value is not specified, the format parameter will be used.
format : str or None
A printf-style format string controlling how the interface should
display numbers. Output must be purely numeric. This does not impact
the return value. Valid formatters: %d %e %f %g %i %u
key : str or int
An optional string or integer to use as the unique key for the widget.
If this is omitted, a key will be generated for the widget
based on its content. Multiple widgets of the same type may
not share the same key.
help : str
An optional tooltip that gets displayed next to the input.
on_change : callable
An optional callback invoked when this number_input's value changes.
args : tuple
An optional tuple of args to pass to the callback.
kwargs : dict
An optional dict of kwargs to pass to the callback.
disabled : bool
An optional boolean, which disables the number input if set to
True. The default is False. This argument can only be supplied by
keyword.
Returns
-------
int or float
The current value of the numeric input widget. The return type
will match the data type of the value parameter.
Example
-------
>>> number = st.number_input('Insert a number')
>>> st.write('The current number is ', number)
.. output::
https://share.streamlit.io/streamlit/docs/main/python/api-examples-source/widget.number_input.py
height: 260px
"""
ctx = get_script_run_ctx()
return self._number_input(
label=label,
min_value=min_value,
max_value=max_value,
value=value,
step=step,
format=format,
key=key,
help=help,
on_change=on_change,
args=args,
kwargs=kwargs,
disabled=disabled,
ctx=ctx,
)
def _number_input(
self,
label: str,
min_value: Optional[Number] = None,
max_value: Optional[Number] = None,
value: Union[NoValue, Number, None] = NoValue(),
step: Optional[Number] = None,
format: Optional[str] = None,
key: Optional[Key] = None,
help: Optional[str] = None,
on_change: Optional[WidgetCallback] = None,
args: Optional[WidgetArgs] = None,
kwargs: Optional[WidgetKwargs] = None,
*, # keyword-only arguments:
disabled: bool = False,
ctx: Optional[ScriptRunContext] = None,
) -> Number:
key = to_key(key)
check_callback_rules(self.dg, on_change)
check_session_state_rules(
default_value=None if isinstance(value, NoValue) else value, key=key
)
# Ensure that all arguments are of the same type.
number_input_args = [min_value, max_value, value, step]
int_args = all(
isinstance(a, (numbers.Integral, type(None), NoValue))
for a in number_input_args
)
float_args = all(
isinstance(a, (float, type(None), NoValue)) for a in number_input_args
)
if not int_args and not float_args:
raise StreamlitAPIException(
"All numerical arguments must be of the same type."
f"\n`value` has {type(value).__name__} type."
f"\n`min_value` has {type(min_value).__name__} type."
f"\n`max_value` has {type(max_value).__name__} type."
f"\n`step` has {type(step).__name__} type."
)
if isinstance(value, NoValue):
if min_value is not None:
value = min_value
elif int_args and float_args:
value = 0.0 # if no values are provided, defaults to float
elif int_args:
value = 0
else:
value = 0.0
int_value = isinstance(value, numbers.Integral)
float_value = isinstance(value, float)
if value is None:
raise StreamlitAPIException(
"Default value for number_input should be an int or a float."
)
else:
if format is None:
format = "%d" if int_value else "%0.2f"
# Warn user if they format an int type as a float or vice versa.
if format in ["%d", "%u", "%i"] and float_value:
import streamlit as st
st.warning(
"Warning: NumberInput value below has type float,"
f" but format {format} displays as integer."
)
elif format[-1] == "f" and int_value:
import streamlit as st
st.warning(
"Warning: NumberInput value below has type int so is"
f" displayed as int despite format string {format}."
)
if step is None:
step = 1 if int_value else 0.01
try:
float(format % 2)
except (TypeError, ValueError):
raise StreamlitAPIException(
"Format string for st.number_input contains invalid characters: %s"
% format
)
# Ensure that the value matches arguments' types.
all_ints = int_value and int_args
if (min_value and min_value > value) or (max_value and max_value < value):
raise StreamlitAPIException(
"The default `value` of %(value)s "
"must lie between the `min_value` of %(min)s "
"and the `max_value` of %(max)s, inclusively."
% {"value": value, "min": min_value, "max": max_value}
)
# Bounds checks. JSNumber produces human-readable exceptions that
# we simply re-package as StreamlitAPIExceptions.
try:
if all_ints:
if min_value is not None:
JSNumber.validate_int_bounds(min_value, "`min_value`") # type: ignore
if max_value is not None:
JSNumber.validate_int_bounds(max_value, "`max_value`") # type: ignore
if step is not None:
JSNumber.validate_int_bounds(step, "`step`") # type: ignore
JSNumber.validate_int_bounds(value, "`value`") # type: ignore
else:
if min_value is not None:
JSNumber.validate_float_bounds(min_value, "`min_value`")
if max_value is not None:
JSNumber.validate_float_bounds(max_value, "`max_value`")
if step is not None:
JSNumber.validate_float_bounds(step, "`step`")
JSNumber.validate_float_bounds(value, "`value`")
except JSNumberBoundsException as e:
raise StreamlitAPIException(str(e))
number_input_proto = NumberInputProto()
number_input_proto.data_type = (
NumberInputProto.INT if all_ints else NumberInputProto.FLOAT
)
number_input_proto.label = label
number_input_proto.default = value
number_input_proto.form_id = current_form_id(self.dg)
if help is not None:
number_input_proto.help = dedent(help)
if min_value is not None:
number_input_proto.min = min_value
number_input_proto.has_min = True
if max_value is not None:
number_input_proto.max = max_value
number_input_proto.has_max = True
if step is not None:
number_input_proto.step = step
if format is not None:
number_input_proto.format = format
def deserialize_number_input(ui_value, widget_id=""):
return ui_value if ui_value is not None else value
current_value, set_frontend_value = register_widget(
"number_input",
number_input_proto,
user_key=key,
on_change_handler=on_change,
args=args,
kwargs=kwargs,
deserializer=deserialize_number_input,
serializer=lambda x: x,
ctx=ctx,
)
# This needs to be done after register_widget because we don't want
# the following proto fields to affect a widget's ID.
number_input_proto.disabled = disabled
if set_frontend_value:
number_input_proto.value = current_value
number_input_proto.set_value = True
self.dg._enqueue("number_input", number_input_proto)
return cast(Number, current_value)
@property
def dg(self) -> "streamlit.delta_generator.DeltaGenerator":
"""Get our DeltaGenerator."""
return cast("streamlit.delta_generator.DeltaGenerator", self)

View File

@@ -0,0 +1,193 @@
# Copyright 2018-2022 Streamlit Inc.
#
# 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."""
import json
import urllib.parse
from typing import cast
import streamlit
from streamlit.legacy_caching import caching
from streamlit import type_util
from streamlit.logger import get_logger
from streamlit.proto.PlotlyChart_pb2 import PlotlyChart as PlotlyChartProto
LOGGER = get_logger(__name__)
SHARING_MODES = {
# This means the plot will be sent to the Streamlit app rather than to
# Plotly.
"streamlit",
# The three modes below are for plots that should be hosted in Plotly.
# These are the names Plotly uses for them.
"private",
"public",
"secret",
}
class PlotlyMixin:
def plotly_chart(
self,
figure_or_data,
use_container_width=False,
sharing="streamlit",
**kwargs,
):
"""Display an interactive Plotly chart.
Plotly is a charting library for Python. The arguments to this function
closely follow the ones for Plotly's `plot()` function. You can find
more about Plotly at https://plot.ly/python.
To show Plotly charts in Streamlit, call `st.plotly_chart` wherever you
would call Plotly's `py.plot` or `py.iplot`.
Parameters
----------
figure_or_data : plotly.graph_objs.Figure, plotly.graph_objs.Data,
dict/list of plotly.graph_objs.Figure/Data
See https://plot.ly/python/ for examples of graph descriptions.
use_container_width : bool
If True, set the chart width to the column width. This takes
precedence over the figure's native `width` value.
sharing : {'streamlit', 'private', 'secret', 'public'}
Use 'streamlit' to insert the plot and all its dependencies
directly in the Streamlit app using plotly's offline mode (default).
Use any other sharing mode to send the chart to Plotly chart studio, which
requires an account. See https://plotly.com/chart-studio/ for more information.
**kwargs
Any argument accepted by Plotly's `plot()` function.
Example
-------
The example below comes straight from the examples at
https://plot.ly/python:
>>> import streamlit as st
>>> import plotly.figure_factory as ff
>>> import numpy as np
>>>
>>> # 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, use_container_width=True)
.. output::
https://share.streamlit.io/streamlit/docs/main/python/api-examples-source/charts.plotly_chart.py
height: 400px
"""
# 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.
plotly_chart_proto = PlotlyChartProto()
marshall(
plotly_chart_proto, figure_or_data, use_container_width, sharing, **kwargs
)
return self.dg._enqueue("plotly_chart", plotly_chart_proto)
@property
def dg(self) -> "streamlit.delta_generator.DeltaGenerator":
"""Get our DeltaGenerator."""
return cast("streamlit.delta_generator.DeltaGenerator", self)
def marshall(proto, figure_or_data, use_container_width, sharing, **kwargs):
"""Marshall a proto with a Plotly spec.
See DeltaGenerator.plotly_chart for docs.
"""
# NOTE: "figure_or_data" is the name used in Plotly's .plot() method
# for their main parameter. I don't like the name, but its best to keep
# it in sync with what Plotly calls it.
import plotly.tools
if type_util.is_type(figure_or_data, "matplotlib.figure.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
)
if not isinstance(sharing, str) or sharing.lower() not in SHARING_MODES:
raise ValueError("Invalid sharing mode for Plotly chart: %s" % sharing)
proto.use_container_width = use_container_width
if sharing == "streamlit":
import plotly.utils
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))
proto.figure.spec = json.dumps(figure, cls=plotly.utils.PlotlyJSONEncoder)
proto.figure.config = json.dumps(config)
else:
url = _plot_to_url_or_load_cached_url(
figure, sharing=sharing, auto_open=False, **kwargs
)
proto.url = _get_embed_url(url)
@caching.cache
def _plot_to_url_or_load_cached_url(*args, **kwargs):
"""Call plotly.plot wrapped in st.cache.
This is so we don't unecessarily upload data to Plotly's SASS if nothing
changed since the previous upload.
"""
try:
# Plotly 4 changed its main package.
import chart_studio.plotly as ply
except ImportError:
import plotly.plotly as ply
return ply.plot(*args, **kwargs)
def _get_embed_url(url):
parsed_url = urllib.parse.urlparse(url)
# Plotly's embed URL is the normal URL plus ".embed".
# (Note that our use namedtuple._replace is fine because that's not a
# private method! It just has an underscore to avoid clashing with the
# tuple field names)
parsed_embed_url = parsed_url._replace(path=parsed_url.path + ".embed")
return urllib.parse.urlunparse(parsed_embed_url)

View File

@@ -0,0 +1,74 @@
# Copyright 2018-2022 Streamlit Inc.
#
# 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 typing import cast
import streamlit
from streamlit.errors import StreamlitAPIException
from streamlit.proto.Progress_pb2 import Progress as ProgressProto
class ProgressMixin:
def progress(self, value):
"""Display a progress bar.
Parameters
----------
value : int or float
0 <= value <= 100 for int
0.0 <= value <= 1.0 for float
Example
-------
Here is an example of a progress bar increasing over time:
>>> import time
>>>
>>> my_bar = st.progress(0)
>>>
>>> for percent_complete in range(100):
... time.sleep(0.1)
... my_bar.progress(percent_complete + 1)
"""
# TODO: standardize numerical type checking across st.* functions.
progress_proto = ProgressProto()
if isinstance(value, float):
if 0.0 <= value <= 1.0:
progress_proto.value = int(value * 100)
else:
raise StreamlitAPIException(
"Progress Value has invalid value [0.0, 1.0]: %f" % value
)
elif isinstance(value, int):
if 0 <= value <= 100:
progress_proto.value = value
else:
raise StreamlitAPIException(
"Progress Value has invalid value [0, 100]: %d" % value
)
else:
raise StreamlitAPIException(
"Progress Value has invalid type: %s" % type(value).__name__
)
return self.dg._enqueue("progress", progress_proto)
@property
def dg(self) -> "streamlit.delta_generator.DeltaGenerator":
"""Get our DeltaGenerator."""
return cast("streamlit.delta_generator.DeltaGenerator", self)

View File

@@ -0,0 +1,170 @@
# Copyright 2018-2022 Streamlit Inc.
#
# 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 Matplotlib PyPlot charts."""
import io
from typing import cast
import streamlit
import streamlit.elements.image as image_utils
from streamlit import config
from streamlit.errors import StreamlitDeprecationWarning
from streamlit.logger import get_logger
from streamlit.proto.Image_pb2 import ImageList as ImageListProto
LOGGER = get_logger(__name__)
class PyplotMixin:
def pyplot(self, fig=None, clear_figure=None, **kwargs):
"""Display a matplotlib.pyplot figure.
Parameters
----------
fig : Matplotlib Figure
The figure to plot. When this argument isn't specified, this
function will render the global figure (but this is deprecated,
as described below)
clear_figure : bool
If True, the figure will be cleared after being rendered.
If False, the figure will not be cleared after being rendered.
If left unspecified, we pick a default based on the value of `fig`.
* If `fig` is set, defaults to `False`.
* If `fig` is not set, defaults to `True`. This simulates Jupyter's
approach to matplotlib rendering.
**kwargs : any
Arguments to pass to Matplotlib's savefig function.
Example
-------
>>> import matplotlib.pyplot as plt
>>> import numpy as np
>>>
>>> arr = np.random.normal(1, 1, size=100)
>>> fig, ax = plt.subplots()
>>> ax.hist(arr, bins=20)
>>>
>>> st.pyplot(fig)
.. output::
https://share.streamlit.io/streamlit/docs/main/python/api-examples-source/charts.pyplot.py
height: 630px
Notes
-----
.. note::
Deprecation warning. After December 1st, 2020, we will remove the ability
to specify no arguments in `st.pyplot()`, as that requires the use of
Matplotlib's global figure object, which is not thread-safe. So
please always pass a figure object as shown in the example section
above.
Matplotlib support several different types of "backends". If you're
getting an error using Matplotlib with Streamlit, try setting your
backend to "TkAgg"::
echo "backend: TkAgg" >> ~/.matplotlib/matplotlibrc
For more information, see https://matplotlib.org/faq/usage_faq.html.
"""
if not fig and config.get_option("deprecation.showPyplotGlobalUse"):
self.dg.exception(PyplotGlobalUseWarning())
image_list_proto = ImageListProto()
marshall(
self.dg._get_delta_path_str(), image_list_proto, fig, clear_figure, **kwargs
)
return self.dg._enqueue("imgs", image_list_proto)
@property
def dg(self) -> "streamlit.delta_generator.DeltaGenerator":
"""Get our DeltaGenerator."""
return cast("streamlit.delta_generator.DeltaGenerator", self)
def marshall(coordinates, image_list_proto, fig=None, clear_figure=True, **kwargs):
try:
import matplotlib
import matplotlib.pyplot as plt
plt.ioff()
except ImportError:
raise ImportError("pyplot() command requires matplotlib")
# You can call .savefig() on a Figure object or directly on the pyplot
# module, in which case you're doing it to the latest Figure.
if not fig:
if clear_figure is None:
clear_figure = True
fig = plt
# Normally, dpi is set to 'figure', and the figure's dpi is set to 100.
# So here we pick double of that to make things look good in a high
# DPI display.
options = {"bbox_inches": "tight", "dpi": 200, "format": "png"}
# If some of the options are passed in from kwargs then replace
# the values in options with the ones from kwargs
options = {a: kwargs.get(a, b) for a, b in options.items()}
# Merge options back into kwargs.
kwargs.update(options)
image = io.BytesIO()
fig.savefig(image, **kwargs)
image_utils.marshall_images(
coordinates,
image,
None,
-2,
image_list_proto,
False,
channels="RGB",
output_format="PNG",
)
# Clear the figure after rendering it. This means that subsequent
# plt calls will be starting fresh.
if clear_figure:
fig.clf()
class PyplotGlobalUseWarning(StreamlitDeprecationWarning):
def __init__(self):
super(PyplotGlobalUseWarning, self).__init__(
msg=self._get_message(), config_option="deprecation.showPyplotGlobalUse"
)
def _get_message(self):
return """
You are calling `st.pyplot()` without any arguments. After December 1st, 2020,
we will remove the ability to do this as it requires the use of Matplotlib's global
figure object, which is not thread-safe.
To future-proof this code, you should pass in a figure as shown below:
```python
>>> fig, ax = plt.subplots()
>>> ax.scatter([1, 2, 3], [1, 2, 3])
>>> ... other plotting actions ...
>>> st.pyplot(fig)
```
"""

View File

@@ -0,0 +1,193 @@
# Copyright 2018-2022 Streamlit Inc.
#
# 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 textwrap import dedent
from typing import Any, Callable, Optional, cast
import streamlit
from streamlit.errors import StreamlitAPIException
from streamlit.proto.Radio_pb2 import Radio as RadioProto
from streamlit.scriptrunner import ScriptRunContext, get_script_run_ctx
from streamlit.state import (
register_widget,
WidgetArgs,
WidgetCallback,
WidgetKwargs,
)
from streamlit.type_util import Key, OptionSequence, ensure_indexable, to_key
from streamlit.util import index_
from .form import current_form_id
from .utils import check_callback_rules, check_session_state_rules
class RadioMixin:
def radio(
self,
label: str,
options: OptionSequence,
index: int = 0,
format_func: Callable[[Any], Any] = str,
key: Optional[Key] = None,
help: Optional[str] = None,
on_change: Optional[WidgetCallback] = None,
args: Optional[WidgetArgs] = None,
kwargs: Optional[WidgetKwargs] = None,
*, # keyword-only args:
disabled: bool = False,
) -> Any:
"""Display a radio button widget.
Parameters
----------
label : str
A short label explaining to the user what this radio group is for.
options : Sequence, numpy.ndarray, pandas.Series, pandas.DataFrame, or pandas.Index
Labels for the radio options. This will be cast to str internally
by default. For pandas.DataFrame, the first column is selected.
index : int
The index of the preselected option on first render.
format_func : function
Function to modify the display of radio options. It receives
the raw option as an argument and should output the label to be
shown for that option. This has no impact on the return value of
the radio.
key : str or int
An optional string or integer to use as the unique key for the widget.
If this is omitted, a key will be generated for the widget
based on its content. Multiple widgets of the same type may
not share the same key.
help : str
An optional tooltip that gets displayed next to the radio.
on_change : callable
An optional callback invoked when this radio's value changes.
args : tuple
An optional tuple of args to pass to the callback.
kwargs : dict
An optional dict of kwargs to pass to the callback.
disabled : bool
An optional boolean, which disables the radio button if set to
True. The default is False. This argument can only be supplied by
keyword.
Returns
-------
any
The selected option.
Example
-------
>>> genre = st.radio(
... "What\'s your favorite movie genre",
... ('Comedy', 'Drama', 'Documentary'))
>>>
>>> if genre == 'Comedy':
... st.write('You selected comedy.')
... else:
... st.write("You didn\'t select comedy.")
.. output::
https://share.streamlit.io/streamlit/docs/main/python/api-examples-source/widget.radio.py
height: 260px
"""
ctx = get_script_run_ctx()
return self._radio(
label=label,
options=options,
index=index,
format_func=format_func,
key=key,
help=help,
on_change=on_change,
args=args,
kwargs=kwargs,
disabled=disabled,
ctx=ctx,
)
def _radio(
self,
label: str,
options: OptionSequence,
index: int = 0,
format_func: Callable[[Any], Any] = str,
key: Optional[Key] = None,
help: Optional[str] = None,
on_change: Optional[WidgetCallback] = None,
args: Optional[WidgetArgs] = None,
kwargs: Optional[WidgetKwargs] = None,
*, # keyword-only args:
disabled: bool = False,
ctx: Optional[ScriptRunContext],
) -> Any:
key = to_key(key)
check_callback_rules(self.dg, on_change)
check_session_state_rules(default_value=None if index == 0 else index, key=key)
opt = ensure_indexable(options)
if not isinstance(index, int):
raise StreamlitAPIException(
"Radio Value has invalid type: %s" % type(index).__name__
)
if len(opt) > 0 and not 0 <= index < len(opt):
raise StreamlitAPIException(
"Radio index must be between 0 and length of options"
)
radio_proto = RadioProto()
radio_proto.label = label
radio_proto.default = index
radio_proto.options[:] = [str(format_func(option)) for option in opt]
radio_proto.form_id = current_form_id(self.dg)
if help is not None:
radio_proto.help = dedent(help)
def deserialize_radio(ui_value, widget_id=""):
idx = ui_value if ui_value is not None else index
return opt[idx] if len(opt) > 0 and opt[idx] is not None else None
def serialize_radio(v):
if len(options) == 0:
return 0
return index_(options, v)
current_value, set_frontend_value = register_widget(
"radio",
radio_proto,
user_key=key,
on_change_handler=on_change,
args=args,
kwargs=kwargs,
deserializer=deserialize_radio,
serializer=serialize_radio,
ctx=ctx,
)
# This needs to be done after register_widget because we don't want
# the following proto fields to affect a widget's ID.
radio_proto.disabled = disabled
if set_frontend_value:
radio_proto.value = serialize_radio(current_value)
radio_proto.set_value = True
self.dg._enqueue("radio", radio_proto)
return cast(str, current_value)
@property
def dg(self) -> "streamlit.delta_generator.DeltaGenerator":
"""Get our DeltaGenerator."""
return cast("streamlit.delta_generator.DeltaGenerator", self)

View File

@@ -0,0 +1,234 @@
# Copyright 2018-2022 Streamlit Inc.
#
# 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 textwrap import dedent
from typing import Any, Callable, Optional, cast
import streamlit
from streamlit.errors import StreamlitAPIException
from streamlit.proto.Slider_pb2 import Slider as SliderProto
from streamlit.scriptrunner import ScriptRunContext, get_script_run_ctx
from streamlit.state import (
register_widget,
WidgetArgs,
WidgetCallback,
WidgetKwargs,
)
from streamlit.type_util import Key, OptionSequence, ensure_indexable, to_key
from streamlit.util import index_
from .form import current_form_id
from .utils import check_callback_rules, check_session_state_rules
class SelectSliderMixin:
def select_slider(
self,
label: str,
options: OptionSequence = [],
value: Any = None,
format_func: Callable[[Any], Any] = str,
key: Optional[Key] = None,
help: Optional[str] = None,
on_change: Optional[WidgetCallback] = None,
args: Optional[WidgetArgs] = None,
kwargs: Optional[WidgetKwargs] = None,
*, # keyword-only arguments:
disabled: bool = False,
) -> Any:
"""
Display a slider widget to select items from a list.
This also allows you to render a range slider by passing a two-element
tuple or list as the `value`.
The difference between `st.select_slider` and `st.slider` is that
`select_slider` accepts any datatype and takes an iterable set of
options, while `slider` only accepts numerical or date/time data and
takes a range as input.
Parameters
----------
label : str
A short label explaining to the user what this slider is for.
options : Sequence, numpy.ndarray, pandas.Series, pandas.DataFrame, or pandas.Index
Labels for the slider options. All options will be cast to str
internally by default. For pandas.DataFrame, the first column is
selected.
value : a supported type or a tuple/list of supported types or None
The value of the slider when it first renders. If a tuple/list
of two values is passed here, then a range slider with those lower
and upper bounds is rendered. For example, if set to `(1, 10)` the
slider will have a selectable range between 1 and 10.
Defaults to first option.
format_func : function
Function to modify the display of the labels from the options.
argument. It receives the option as an argument and its output
will be cast to str.
key : str or int
An optional string or integer to use as the unique key for the widget.
If this is omitted, a key will be generated for the widget
based on its content. Multiple widgets of the same type may
not share the same key.
help : str
An optional tooltip that gets displayed next to the select slider.
on_change : callable
An optional callback invoked when this select_slider's value changes.
args : tuple
An optional tuple of args to pass to the callback.
kwargs : dict
An optional dict of kwargs to pass to the callback.
disabled : bool
An optional boolean, which disables the select slider if set to True.
The default is False. This argument can only be supplied by keyword.
Returns
-------
any value or tuple of any value
The current value of the slider widget. The return type will match
the data type of the value parameter.
Examples
--------
>>> color = st.select_slider(
... 'Select a color of the rainbow',
... options=['red', 'orange', 'yellow', 'green', 'blue', 'indigo', 'violet'])
>>> st.write('My favorite color is', color)
And here's an example of a range select slider:
>>> start_color, end_color = st.select_slider(
... 'Select a range of color wavelength',
... options=['red', 'orange', 'yellow', 'green', 'blue', 'indigo', 'violet'],
... value=('red', 'blue'))
>>> st.write('You selected wavelengths between', start_color, 'and', end_color)
.. output::
https://share.streamlit.io/streamlit/docs/main/python/api-examples-source/widget.select_slider.py
height: 450px
"""
ctx = get_script_run_ctx()
return self._select_slider(
label=label,
options=options,
value=value,
format_func=format_func,
key=key,
help=help,
on_change=on_change,
args=args,
kwargs=kwargs,
disabled=disabled,
ctx=ctx,
)
def _select_slider(
self,
label: str,
options: OptionSequence = [],
value: Any = None,
format_func: Callable[[Any], Any] = str,
key: Optional[Key] = None,
help: Optional[str] = None,
on_change: Optional[WidgetCallback] = None,
args: Optional[WidgetArgs] = None,
kwargs: Optional[WidgetKwargs] = None,
disabled: bool = False,
ctx: Optional[ScriptRunContext] = None,
) -> Any:
key = to_key(key)
check_callback_rules(self.dg, on_change)
check_session_state_rules(default_value=value, key=key)
opt = ensure_indexable(options)
if len(opt) == 0:
raise StreamlitAPIException("The `options` argument needs to be non-empty")
is_range_value = isinstance(value, (list, tuple))
def as_index_list(v):
is_range_value = isinstance(v, (list, tuple))
if is_range_value:
slider_value = [index_(opt, val) for val in v]
start, end = slider_value
if start > end:
slider_value = [end, start]
return slider_value
else:
# Simplify future logic by always making value a list
try:
return [index_(opt, v)]
except ValueError:
if value is not None:
raise
return [0]
# Convert element to index of the elements
slider_value = as_index_list(value)
slider_proto = SliderProto()
slider_proto.label = label
slider_proto.format = "%s"
slider_proto.default[:] = slider_value
slider_proto.min = 0
slider_proto.max = len(opt) - 1
slider_proto.step = 1 # default for index changes
slider_proto.data_type = SliderProto.INT
slider_proto.options[:] = [str(format_func(option)) for option in opt]
slider_proto.form_id = current_form_id(self.dg)
if help is not None:
slider_proto.help = dedent(help)
def deserialize_select_slider(ui_value, widget_id=""):
if not ui_value:
# Widget has not been used; fallback to the original value,
ui_value = slider_value
# The widget always returns floats, so convert to ints before indexing
return_value = list(map(lambda x: opt[int(x)], ui_value)) # type: ignore[no-any-return]
# If the original value was a list/tuple, so will be the output (and vice versa)
return tuple(return_value) if is_range_value else return_value[0]
def serialize_select_slider(v):
return as_index_list(v)
current_value, set_frontend_value = register_widget(
"slider",
slider_proto,
user_key=key,
on_change_handler=on_change,
args=args,
kwargs=kwargs,
deserializer=deserialize_select_slider,
serializer=serialize_select_slider,
ctx=ctx,
)
# This needs to be done after register_widget because we don't want
# the following proto fields to affect a widget's ID.
slider_proto.disabled = disabled
if set_frontend_value:
slider_proto.value[:] = serialize_select_slider(current_value)
slider_proto.set_value = True
self.dg._enqueue("slider", slider_proto)
return current_value
@property
def dg(self) -> "streamlit.delta_generator.DeltaGenerator":
"""Get our DeltaGenerator."""
return cast("streamlit.delta_generator.DeltaGenerator", self)

View File

@@ -0,0 +1,187 @@
# Copyright 2018-2022 Streamlit Inc.
#
# 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 textwrap import dedent
from typing import Any, Callable, Optional, cast
import streamlit
from streamlit.errors import StreamlitAPIException
from streamlit.proto.Selectbox_pb2 import Selectbox as SelectboxProto
from streamlit.scriptrunner import ScriptRunContext, get_script_run_ctx
from streamlit.state import (
register_widget,
WidgetArgs,
WidgetCallback,
WidgetKwargs,
)
from streamlit.type_util import Key, OptionSequence, ensure_indexable, to_key
from streamlit.util import index_
from .form import current_form_id
from .utils import check_callback_rules, check_session_state_rules
class SelectboxMixin:
def selectbox(
self,
label: str,
options: OptionSequence,
index: int = 0,
format_func: Callable[[Any], Any] = str,
key: Optional[Key] = None,
help: Optional[str] = None,
on_change: Optional[WidgetCallback] = None,
args: Optional[WidgetArgs] = None,
kwargs: Optional[WidgetKwargs] = None,
*, # keyword-only arguments:
disabled: bool = False,
) -> Any:
"""Display a select widget.
Parameters
----------
label : str
A short label explaining to the user what this select widget is for.
options : Sequence, numpy.ndarray, pandas.Series, pandas.DataFrame, or pandas.Index
Labels for the select options. This will be cast to str internally
by default. For pandas.DataFrame, the first column is selected.
index : int
The index of the preselected option on first render.
format_func : function
Function to modify the display of the labels. It receives the option
as an argument and its output will be cast to str.
key : str or int
An optional string or integer to use as the unique key for the widget.
If this is omitted, a key will be generated for the widget
based on its content. Multiple widgets of the same type may
not share the same key.
help : str
An optional tooltip that gets displayed next to the selectbox.
on_change : callable
An optional callback invoked when this selectbox's value changes.
args : tuple
An optional tuple of args to pass to the callback.
kwargs : dict
An optional dict of kwargs to pass to the callback.
disabled : bool
An optional boolean, which disables the selectbox if set to True.
The default is False. This argument can only be supplied by keyword.
Returns
-------
any
The selected option
Example
-------
>>> option = st.selectbox(
... 'How would you like to be contacted?',
... ('Email', 'Home phone', 'Mobile phone'))
>>>
>>> st.write('You selected:', option)
.. output::
https://share.streamlit.io/streamlit/docs/main/python/api-examples-source/widget.selectbox.py
height: 320px
"""
ctx = get_script_run_ctx()
return self._selectbox(
label=label,
options=options,
index=index,
format_func=format_func,
key=key,
help=help,
on_change=on_change,
args=args,
kwargs=kwargs,
disabled=disabled,
ctx=ctx,
)
def _selectbox(
self,
label: str,
options: OptionSequence,
index: int = 0,
format_func: Callable[[Any], Any] = str,
key: Optional[Key] = None,
help: Optional[str] = None,
on_change: Optional[WidgetCallback] = None,
args: Optional[WidgetArgs] = None,
kwargs: Optional[WidgetKwargs] = None,
*, # keyword-only arguments:
disabled: bool = False,
ctx: Optional[ScriptRunContext] = None,
) -> Any:
key = to_key(key)
check_callback_rules(self.dg, on_change)
check_session_state_rules(default_value=None if index == 0 else index, key=key)
opt = ensure_indexable(options)
if not isinstance(index, int):
raise StreamlitAPIException(
"Selectbox Value has invalid type: %s" % type(index).__name__
)
if len(opt) > 0 and not 0 <= index < len(opt):
raise StreamlitAPIException(
"Selectbox index must be between 0 and length of options"
)
selectbox_proto = SelectboxProto()
selectbox_proto.label = label
selectbox_proto.default = index
selectbox_proto.options[:] = [str(format_func(option)) for option in opt]
selectbox_proto.form_id = current_form_id(self.dg)
if help is not None:
selectbox_proto.help = dedent(help)
def deserialize_select_box(ui_value, widget_id=""):
idx = ui_value if ui_value is not None else index
return opt[idx] if len(opt) > 0 and opt[idx] is not None else None
def serialize_select_box(v):
if len(opt) == 0:
return 0
return index_(opt, v)
current_value, set_frontend_value = register_widget(
"selectbox",
selectbox_proto,
user_key=key,
on_change_handler=on_change,
args=args,
kwargs=kwargs,
deserializer=deserialize_select_box,
serializer=serialize_select_box,
ctx=ctx,
)
# This needs to be done after register_widget because we don't want
# the following proto fields to affect a widget's ID.
selectbox_proto.disabled = disabled
if set_frontend_value:
selectbox_proto.value = serialize_select_box(current_value)
selectbox_proto.set_value = True
self.dg._enqueue("selectbox", selectbox_proto)
return cast(str, current_value)
@property
def dg(self) -> "streamlit.delta_generator.DeltaGenerator":
"""Get our DeltaGenerator."""
return cast("streamlit.delta_generator.DeltaGenerator", self)

View File

@@ -0,0 +1,508 @@
# Copyright 2018-2022 Streamlit Inc.
#
# 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 datetime import date, time, datetime, timedelta, timezone
from streamlit.scriptrunner import ScriptRunContext, get_script_run_ctx
from streamlit.type_util import Key, to_key
from typing import Any, List, cast, Optional
from textwrap import dedent
import streamlit
from streamlit.errors import StreamlitAPIException
from streamlit.js_number import JSNumber
from streamlit.js_number import JSNumberBoundsException
from streamlit.proto.Slider_pb2 import Slider as SliderProto
from streamlit.state import (
register_widget,
WidgetArgs,
WidgetCallback,
WidgetKwargs,
)
from .form import current_form_id
from .utils import check_callback_rules, check_session_state_rules
class SliderMixin:
def slider(
self,
label: str,
min_value=None,
max_value=None,
value=None,
step=None,
format=None,
key: Optional[Key] = None,
help: Optional[str] = None,
on_change: Optional[WidgetCallback] = None,
args: Optional[WidgetArgs] = None,
kwargs: Optional[WidgetKwargs] = None,
*, # keyword-only arguments:
disabled: bool = False,
):
"""Display a slider widget.
This supports int, float, date, time, and datetime types.
This also allows you to render a range slider by passing a two-element
tuple or list as the `value`.
The difference between `st.slider` and `st.select_slider` is that
`slider` only accepts numerical or date/time data and takes a range as
input, while `select_slider` accepts any datatype and takes an iterable
set of options.
Parameters
----------
label : str
A short label explaining to the user what this slider is for.
min_value : a supported type or None
The minimum permitted value.
Defaults to 0 if the value is an int, 0.0 if a float,
value - timedelta(days=14) if a date/datetime, time.min if a time
max_value : a supported type or None
The maximum permitted value.
Defaults to 100 if the value is an int, 1.0 if a float,
value + timedelta(days=14) if a date/datetime, time.max if a time
value : a supported type or a tuple/list of supported types or None
The value of the slider when it first renders. If a tuple/list
of two values is passed here, then a range slider with those lower
and upper bounds is rendered. For example, if set to `(1, 10)` the
slider will have a selectable range between 1 and 10.
Defaults to min_value.
step : int/float/timedelta or None
The stepping interval.
Defaults to 1 if the value is an int, 0.01 if a float,
timedelta(days=1) if a date/datetime, timedelta(minutes=15) if a time
(or if max_value - min_value < 1 day)
format : str or None
A printf-style format string controlling how the interface should
display numbers. This does not impact the return value.
Formatter for int/float supports: %d %e %f %g %i
Formatter for date/time/datetime uses Moment.js notation:
https://momentjs.com/docs/#/displaying/format/
key : str or int
An optional string or integer to use as the unique key for the widget.
If this is omitted, a key will be generated for the widget
based on its content. Multiple widgets of the same type may
not share the same key.
help : str
An optional tooltip that gets displayed next to the slider.
on_change : callable
An optional callback invoked when this slider's value changes.
args : tuple
An optional tuple of args to pass to the callback.
kwargs : dict
An optional dict of kwargs to pass to the callback.
disabled : bool
An optional boolean, which disables the slider if set to True. The
default is False. This argument can only be supplied by keyword.
Returns
-------
int/float/date/time/datetime or tuple of int/float/date/time/datetime
The current value of the slider widget. The return type will match
the data type of the value parameter.
Examples
--------
>>> age = st.slider('How old are you?', 0, 130, 25)
>>> st.write("I'm ", age, 'years old')
And here's an example of a range slider:
>>> values = st.slider(
... 'Select a range of values',
... 0.0, 100.0, (25.0, 75.0))
>>> st.write('Values:', values)
This is a range time slider:
>>> from datetime import time
>>> appointment = st.slider(
... "Schedule your appointment:",
... value=(time(11, 30), time(12, 45)))
>>> st.write("You're scheduled for:", appointment)
Finally, a datetime slider:
>>> from datetime import datetime
>>> start_time = st.slider(
... "When do you start?",
... value=datetime(2020, 1, 1, 9, 30),
... format="MM/DD/YY - hh:mm")
>>> st.write("Start time:", start_time)
.. output::
https://share.streamlit.io/streamlit/docs/main/python/api-examples-source/widget.slider.py
height: 300px
"""
ctx = get_script_run_ctx()
return self._slider(
label=label,
min_value=min_value,
max_value=max_value,
value=value,
step=step,
format=format,
key=key,
help=help,
on_change=on_change,
args=args,
kwargs=kwargs,
disabled=disabled,
ctx=ctx,
)
def _slider(
self,
label: str,
min_value=None,
max_value=None,
value=None,
step=None,
format=None,
key: Optional[Key] = None,
help: Optional[str] = None,
on_change: Optional[WidgetCallback] = None,
args: Optional[WidgetArgs] = None,
kwargs: Optional[WidgetKwargs] = None,
*, # keyword-only arguments:
disabled: bool = False,
ctx: Optional[ScriptRunContext] = None,
):
key = to_key(key)
check_callback_rules(self.dg, on_change)
check_session_state_rules(default_value=value, key=key)
# Set value default.
if value is None:
value = min_value if min_value is not None else 0
SUPPORTED_TYPES = {
int: SliderProto.INT,
float: SliderProto.FLOAT,
datetime: SliderProto.DATETIME,
date: SliderProto.DATE,
time: SliderProto.TIME,
}
TIMELIKE_TYPES = (SliderProto.DATETIME, SliderProto.TIME, SliderProto.DATE)
# Ensure that the value is either a single value or a range of values.
single_value = isinstance(value, tuple(SUPPORTED_TYPES.keys()))
range_value = isinstance(value, (list, tuple)) and len(value) in (0, 1, 2)
if not single_value and not range_value:
raise StreamlitAPIException(
"Slider value should either be an int/float/datetime or a list/tuple of "
"0 to 2 ints/floats/datetimes"
)
# Simplify future logic by always making value a list
if single_value:
value = [value]
def all_same_type(items):
return len(set(map(type, items))) < 2
if not all_same_type(value):
raise StreamlitAPIException(
"Slider tuple/list components must be of the same type.\n"
f"But were: {list(map(type, value))}"
)
if len(value) == 0:
data_type = SliderProto.INT
else:
data_type = SUPPORTED_TYPES[type(value[0])]
datetime_min = time.min
datetime_max = time.max
if data_type == SliderProto.TIME:
datetime_min = time.min.replace(tzinfo=value[0].tzinfo)
datetime_max = time.max.replace(tzinfo=value[0].tzinfo)
if data_type in (SliderProto.DATETIME, SliderProto.DATE):
datetime_min = value[0] - timedelta(days=14)
datetime_max = value[0] + timedelta(days=14)
DEFAULTS = {
SliderProto.INT: {
"min_value": 0,
"max_value": 100,
"step": 1,
"format": "%d",
},
SliderProto.FLOAT: {
"min_value": 0.0,
"max_value": 1.0,
"step": 0.01,
"format": "%0.2f",
},
SliderProto.DATETIME: {
"min_value": datetime_min,
"max_value": datetime_max,
"step": timedelta(days=1),
"format": "YYYY-MM-DD",
},
SliderProto.DATE: {
"min_value": datetime_min,
"max_value": datetime_max,
"step": timedelta(days=1),
"format": "YYYY-MM-DD",
},
SliderProto.TIME: {
"min_value": datetime_min,
"max_value": datetime_max,
"step": timedelta(minutes=15),
"format": "HH:mm",
},
}
if min_value is None:
min_value = DEFAULTS[data_type]["min_value"]
if max_value is None:
max_value = DEFAULTS[data_type]["max_value"]
if step is None:
step = DEFAULTS[data_type]["step"]
if (
data_type
in (
SliderProto.DATETIME,
SliderProto.DATE,
)
and max_value - min_value < timedelta(days=1)
):
step = timedelta(minutes=15)
if format is None:
format = DEFAULTS[data_type]["format"]
if step == 0:
raise StreamlitAPIException(
"Slider components cannot be passed a `step` of 0."
)
# Ensure that all arguments are of the same type.
slider_args = [min_value, max_value, step]
int_args = all(map(lambda a: isinstance(a, int), slider_args))
float_args = all(map(lambda a: isinstance(a, float), slider_args))
# When min and max_value are the same timelike, step should be a timedelta
timelike_args = (
data_type in TIMELIKE_TYPES
and isinstance(step, timedelta)
and type(min_value) == type(max_value)
)
if not int_args and not float_args and not timelike_args:
raise StreamlitAPIException(
"Slider value arguments must be of matching types."
"\n`min_value` has %(min_type)s type."
"\n`max_value` has %(max_type)s type."
"\n`step` has %(step)s type."
% {
"min_type": type(min_value).__name__,
"max_type": type(max_value).__name__,
"step": type(step).__name__,
}
)
# Ensure that the value matches arguments' types.
all_ints = data_type == SliderProto.INT and int_args
all_floats = data_type == SliderProto.FLOAT and float_args
all_timelikes = data_type in TIMELIKE_TYPES and timelike_args
if not all_ints and not all_floats and not all_timelikes:
raise StreamlitAPIException(
"Both value and arguments must be of the same type."
"\n`value` has %(value_type)s type."
"\n`min_value` has %(min_type)s type."
"\n`max_value` has %(max_type)s type."
% {
"value_type": type(value).__name__,
"min_type": type(min_value).__name__,
"max_type": type(max_value).__name__,
}
)
# Ensure that min <= value(s) <= max, adjusting the bounds as necessary.
min_value = min(min_value, max_value)
max_value = max(min_value, max_value)
if len(value) == 1:
min_value = min(value[0], min_value)
max_value = max(value[0], max_value)
elif len(value) == 2:
start, end = value
if start > end:
# Swap start and end, since they seem reversed
start, end = end, start
value = start, end
min_value = min(start, min_value)
max_value = max(end, max_value)
else:
# Empty list, so let's just use the outer bounds
value = [min_value, max_value]
# Bounds checks. JSNumber produces human-readable exceptions that
# we simply re-package as StreamlitAPIExceptions.
# (We check `min_value` and `max_value` here; `value` and `step` are
# already known to be in the [min_value, max_value] range.)
try:
if all_ints:
JSNumber.validate_int_bounds(min_value, "`min_value`")
JSNumber.validate_int_bounds(max_value, "`max_value`")
elif all_floats:
JSNumber.validate_float_bounds(min_value, "`min_value`")
JSNumber.validate_float_bounds(max_value, "`max_value`")
elif all_timelikes:
# No validation yet. TODO: check between 0001-01-01 to 9999-12-31
pass
except JSNumberBoundsException as e:
raise StreamlitAPIException(str(e))
# Convert dates or times into datetimes
if data_type == SliderProto.TIME:
def _time_to_datetime(time):
# Note, here we pick an arbitrary date well after Unix epoch.
# This prevents pre-epoch timezone issues (https://bugs.python.org/issue36759)
# We're dropping the date from datetime laters, anyways.
return datetime.combine(date(2000, 1, 1), time)
value = list(map(_time_to_datetime, value))
min_value = _time_to_datetime(min_value)
max_value = _time_to_datetime(max_value)
if data_type == SliderProto.DATE:
def _date_to_datetime(date):
return datetime.combine(date, time())
value = list(map(_date_to_datetime, value))
min_value = _date_to_datetime(min_value)
max_value = _date_to_datetime(max_value)
# Now, convert to microseconds (so we can serialize datetime to a long)
if data_type in TIMELIKE_TYPES:
SECONDS_TO_MICROS = 1000 * 1000
DAYS_TO_MICROS = 24 * 60 * 60 * SECONDS_TO_MICROS
def _delta_to_micros(delta):
return (
delta.microseconds
+ delta.seconds * SECONDS_TO_MICROS
+ delta.days * DAYS_TO_MICROS
)
UTC_EPOCH = datetime(1970, 1, 1, tzinfo=timezone.utc)
def _datetime_to_micros(dt):
# The frontend is not aware of timezones and only expects a UTC-based timestamp (in microseconds).
# Since we want to show the date/time exactly as it is in the given datetime object,
# we just set the tzinfo to UTC and do not do any timezone conversions.
# Only the backend knows about original timezone and will replace the UTC timestamp in the deserialization.
utc_dt = dt.replace(tzinfo=timezone.utc)
return _delta_to_micros(utc_dt - UTC_EPOCH)
# Restore times/datetimes to original timezone (dates are always naive)
orig_tz = (
value[0].tzinfo
if data_type in (SliderProto.TIME, SliderProto.DATETIME)
else None
)
def _micros_to_datetime(micros):
utc_dt = UTC_EPOCH + timedelta(microseconds=micros)
# Add the original timezone. No conversion is required here,
# since in the serialization, we also just replace the timestamp with UTC.
return utc_dt.replace(tzinfo=orig_tz)
value = list(map(_datetime_to_micros, value))
min_value = _datetime_to_micros(min_value)
max_value = _datetime_to_micros(max_value)
step = _delta_to_micros(step)
# It would be great if we could guess the number of decimal places from
# the `step` argument, but this would only be meaningful if step were a
# decimal. As a possible improvement we could make this function accept
# decimals and/or use some heuristics for floats.
slider_proto = SliderProto()
slider_proto.label = label
slider_proto.format = format
slider_proto.default[:] = value
slider_proto.min = min_value
slider_proto.max = max_value
slider_proto.step = step
slider_proto.data_type = data_type
slider_proto.options[:] = []
slider_proto.form_id = current_form_id(self.dg)
if help is not None:
slider_proto.help = dedent(help)
def deserialize_slider(ui_value: Optional[List[float]], widget_id=""):
if ui_value is not None:
val = ui_value
else:
# Widget has not been used; fallback to the original value,
val = cast(List[float], value)
# The widget always returns a float array, so fix the return type if necessary
if data_type == SliderProto.INT:
val = [int(v) for v in val]
if data_type == SliderProto.DATETIME:
val = [_micros_to_datetime(int(v)) for v in val]
if data_type == SliderProto.DATE:
val = [_micros_to_datetime(int(v)).date() for v in val]
if data_type == SliderProto.TIME:
val = [
_micros_to_datetime(int(v)).time().replace(tzinfo=orig_tz)
for v in val
]
return val[0] if single_value else tuple(val)
def serialize_slider(v: Any) -> List[Any]:
range_value = isinstance(v, (list, tuple))
value = list(v) if range_value else [v]
if data_type == SliderProto.DATE:
value = [_datetime_to_micros(_date_to_datetime(v)) for v in value]
if data_type == SliderProto.TIME:
value = [_datetime_to_micros(_time_to_datetime(v)) for v in value]
if data_type == SliderProto.DATETIME:
value = [_datetime_to_micros(v) for v in value]
return value
current_value, set_frontend_value = register_widget(
"slider",
slider_proto,
user_key=key,
on_change_handler=on_change,
args=args,
kwargs=kwargs,
deserializer=deserialize_slider,
serializer=serialize_slider,
ctx=ctx,
)
# This needs to be done after register_widget because we don't want
# the following proto fields to affect a widget's ID.
slider_proto.disabled = disabled
if set_frontend_value:
slider_proto.value[:] = serialize_slider(current_value)
slider_proto.set_value = True
self.dg._enqueue("slider", slider_proto)
return current_value
@property
def dg(self) -> "streamlit.delta_generator.DeltaGenerator":
"""Get our DeltaGenerator."""
return cast("streamlit.delta_generator.DeltaGenerator", self)

View File

@@ -0,0 +1,39 @@
# Copyright 2018-2022 Streamlit Inc.
#
# 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 typing import cast
import streamlit
from streamlit.proto.Snow_pb2 import Snow as SnowProto
class SnowMixin:
def snow(self):
"""Draw celebratory snowfall.
Example
-------
>>> st.snow()
...then watch your app and get ready for a cool celebration!
"""
snow_proto = SnowProto()
snow_proto.show = True
return self.dg._enqueue("snow", snow_proto)
@property
def dg(self) -> "streamlit.delta_generator.DeltaGenerator":
"""Get our DeltaGenerator."""
return cast("streamlit.delta_generator.DeltaGenerator", self)

View File

@@ -0,0 +1,43 @@
# Copyright 2018-2022 Streamlit Inc.
#
# 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 typing import cast
import streamlit
from streamlit.proto.Text_pb2 import Text as TextProto
from .utils import clean_text
class TextMixin:
def text(self, body):
"""Write fixed-width and preformatted text.
Parameters
----------
body : str
The string to display.
Example
-------
>>> st.text('This is some text.')
"""
text_proto = TextProto()
text_proto.body = clean_text(body)
return self.dg._enqueue("text", text_proto)
@property
def dg(self) -> "streamlit.delta_generator.DeltaGenerator":
"""Get our DeltaGenerator."""
return cast("streamlit.delta_generator.DeltaGenerator", self)

View File

@@ -0,0 +1,347 @@
# Copyright 2018-2022 Streamlit Inc.
#
# 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 streamlit.scriptrunner import ScriptRunContext, get_script_run_ctx
from streamlit.type_util import Key, to_key
from textwrap import dedent
from typing import Optional, cast
import streamlit
from streamlit.errors import StreamlitAPIException
from streamlit.proto.TextArea_pb2 import TextArea as TextAreaProto
from streamlit.proto.TextInput_pb2 import TextInput as TextInputProto
from streamlit.state import (
register_widget,
WidgetArgs,
WidgetCallback,
WidgetKwargs,
)
from .form import current_form_id
from .utils import check_callback_rules, check_session_state_rules
class TextWidgetsMixin:
def text_input(
self,
label: str,
value: str = "",
max_chars: Optional[int] = None,
key: Optional[Key] = None,
type: str = "default",
help: Optional[str] = None,
autocomplete: Optional[str] = None,
on_change: Optional[WidgetCallback] = None,
args: Optional[WidgetArgs] = None,
kwargs: Optional[WidgetKwargs] = None,
*, # keyword-only arguments:
placeholder: Optional[str] = None,
disabled: bool = False,
) -> str:
"""Display a single-line text input widget.
Parameters
----------
label : str
A short label explaining to the user what this input is for.
value : any
The text value of this widget when it first renders. This will be
cast to str internally.
max_chars : int or None
Max number of characters allowed in text input.
key : str or int
An optional string or integer to use as the unique key for the widget.
If this is omitted, a key will be generated for the widget
based on its content. Multiple widgets of the same type may
not share the same key.
type : str
The type of the text input. This can be either "default" (for
a regular text input), or "password" (for a text input that
masks the user's typed value). Defaults to "default".
help : str
An optional tooltip that gets displayed next to the input.
autocomplete : str
An optional value that will be passed to the <input> element's
autocomplete property. If unspecified, this value will be set to
"new-password" for "password" inputs, and the empty string for
"default" inputs. For more details, see https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/autocomplete
on_change : callable
An optional callback invoked when this text_input's value changes.
args : tuple
An optional tuple of args to pass to the callback.
kwargs : dict
An optional dict of kwargs to pass to the callback.
placeholder : str or None
An optional string displayed when the text input is empty. If None,
no text is displayed. This argument can only be supplied by keyword.
disabled : bool
An optional boolean, which disables the text input if set to True.
The default is False. This argument can only be supplied by keyword.
Returns
-------
str
The current value of the text input widget.
Example
-------
>>> title = st.text_input('Movie title', 'Life of Brian')
>>> st.write('The current movie title is', title)
.. output::
https://share.streamlit.io/streamlit/docs/main/python/api-examples-source/widget.text_input.py
height: 260px
"""
ctx = get_script_run_ctx()
return self._text_input(
label=label,
value=value,
max_chars=max_chars,
key=key,
type=type,
help=help,
autocomplete=autocomplete,
on_change=on_change,
args=args,
kwargs=kwargs,
placeholder=placeholder,
disabled=disabled,
ctx=ctx,
)
def _text_input(
self,
label: str,
value: str = "",
max_chars: Optional[int] = None,
key: Optional[Key] = None,
type: str = "default",
help: Optional[str] = None,
autocomplete: Optional[str] = None,
on_change: Optional[WidgetCallback] = None,
args: Optional[WidgetArgs] = None,
kwargs: Optional[WidgetKwargs] = None,
*, # keyword-only arguments:
placeholder: Optional[str] = None,
disabled: bool = False,
ctx: Optional[ScriptRunContext] = None,
) -> str:
key = to_key(key)
check_callback_rules(self.dg, on_change)
check_session_state_rules(default_value=None if value == "" else value, key=key)
text_input_proto = TextInputProto()
text_input_proto.label = label
text_input_proto.default = str(value)
text_input_proto.form_id = current_form_id(self.dg)
if help is not None:
text_input_proto.help = dedent(help)
if max_chars is not None:
text_input_proto.max_chars = max_chars
if placeholder is not None:
text_input_proto.placeholder = str(placeholder)
if type == "default":
text_input_proto.type = TextInputProto.DEFAULT
elif type == "password":
text_input_proto.type = TextInputProto.PASSWORD
else:
raise StreamlitAPIException(
"'%s' is not a valid text_input type. Valid types are 'default' and 'password'."
% type
)
# Marshall the autocomplete param. If unspecified, this will be
# set to "new-password" for password inputs.
if autocomplete is None:
autocomplete = "new-password" if type == "password" else ""
text_input_proto.autocomplete = autocomplete
def deserialize_text_input(ui_value, widget_id="") -> str:
return str(ui_value if ui_value is not None else value)
current_value, set_frontend_value = register_widget(
"text_input",
text_input_proto,
user_key=key,
on_change_handler=on_change,
args=args,
kwargs=kwargs,
deserializer=deserialize_text_input,
serializer=lambda x: x,
ctx=ctx,
)
# This needs to be done after register_widget because we don't want
# the following proto fields to affect a widget's ID.
text_input_proto.disabled = disabled
if set_frontend_value:
text_input_proto.value = current_value
text_input_proto.set_value = True
self.dg._enqueue("text_input", text_input_proto)
return cast(str, current_value)
def text_area(
self,
label: str,
value: str = "",
height: Optional[int] = None,
max_chars: Optional[int] = None,
key: Optional[Key] = None,
help: Optional[str] = None,
on_change: Optional[WidgetCallback] = None,
args: Optional[WidgetArgs] = None,
kwargs: Optional[WidgetKwargs] = None,
*, # keyword-only arguments:
placeholder: Optional[str] = None,
disabled: bool = False,
) -> str:
"""Display a multi-line text input widget.
Parameters
----------
label : str
A short label explaining to the user what this input is for.
value : any
The text value of this widget when it first renders. This will be
cast to str internally.
height : int or None
Desired height of the UI element expressed in pixels. If None, a
default height is used.
max_chars : int or None
Maximum number of characters allowed in text area.
key : str or int
An optional string or integer to use as the unique key for the widget.
If this is omitted, a key will be generated for the widget
based on its content. Multiple widgets of the same type may
not share the same key.
help : str
An optional tooltip that gets displayed next to the textarea.
on_change : callable
An optional callback invoked when this text_area's value changes.
args : tuple
An optional tuple of args to pass to the callback.
kwargs : dict
An optional dict of kwargs to pass to the callback.
placeholder : str or None
An optional string displayed when the text area is empty. If None,
no text is displayed. This argument can only be supplied by keyword.
disabled : bool
An optional boolean, which disables the text area if set to True.
The default is False. This argument can only be supplied by keyword.
Returns
-------
str
The current value of the text input widget.
Example
-------
>>> txt = st.text_area('Text to analyze', '''
... It was the best of times, it was the worst of times, it was
... the age of wisdom, it was the age of foolishness, it was
... the epoch of belief, it was the epoch of incredulity, it
... was the season of Light, it was the season of Darkness, it
... was the spring of hope, it was the winter of despair, (...)
... ''')
>>> st.write('Sentiment:', run_sentiment_analysis(txt))
"""
ctx = get_script_run_ctx()
return self._text_area(
label=label,
value=value,
height=height,
max_chars=max_chars,
key=key,
help=help,
on_change=on_change,
args=args,
kwargs=kwargs,
placeholder=placeholder,
disabled=disabled,
ctx=ctx,
)
def _text_area(
self,
label: str,
value: str = "",
height: Optional[int] = None,
max_chars: Optional[int] = None,
key: Optional[Key] = None,
help: Optional[str] = None,
on_change: Optional[WidgetCallback] = None,
args: Optional[WidgetArgs] = None,
kwargs: Optional[WidgetKwargs] = None,
*, # keyword-only arguments:
placeholder: Optional[str] = None,
disabled: bool = False,
ctx: Optional[ScriptRunContext] = None,
) -> str:
key = to_key(key)
check_callback_rules(self.dg, on_change)
check_session_state_rules(default_value=None if value == "" else value, key=key)
text_area_proto = TextAreaProto()
text_area_proto.label = label
text_area_proto.default = str(value)
text_area_proto.form_id = current_form_id(self.dg)
if help is not None:
text_area_proto.help = dedent(help)
if height is not None:
text_area_proto.height = height
if max_chars is not None:
text_area_proto.max_chars = max_chars
if placeholder is not None:
text_area_proto.placeholder = str(placeholder)
def deserialize_text_area(ui_value, widget_id="") -> str:
return str(ui_value if ui_value is not None else value)
current_value, set_frontend_value = register_widget(
"text_area",
text_area_proto,
user_key=key,
on_change_handler=on_change,
args=args,
kwargs=kwargs,
deserializer=deserialize_text_area,
serializer=lambda x: x,
ctx=ctx,
)
# This needs to be done after register_widget because we don't want
# the following proto fields to affect a widget's ID.
text_area_proto.disabled = disabled
if set_frontend_value:
text_area_proto.value = current_value
text_area_proto.set_value = True
self.dg._enqueue("text_area", text_area_proto)
return cast(str, current_value)
@property
def dg(self) -> "streamlit.delta_generator.DeltaGenerator":
"""Get our DeltaGenerator."""
return cast("streamlit.delta_generator.DeltaGenerator", self)

View File

@@ -0,0 +1,370 @@
# Copyright 2018-2022 Streamlit Inc.
#
# 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 datetime import datetime, date, time
from streamlit.scriptrunner import ScriptRunContext, get_script_run_ctx
from streamlit.type_util import Key, to_key
from typing import cast, Optional, Union, Tuple
from textwrap import dedent
from dateutil import relativedelta
import streamlit
from streamlit.errors import StreamlitAPIException
from streamlit.proto.DateInput_pb2 import DateInput as DateInputProto
from streamlit.proto.TimeInput_pb2 import TimeInput as TimeInputProto
from streamlit.state import (
register_widget,
WidgetArgs,
WidgetCallback,
WidgetKwargs,
)
from .form import current_form_id
from .utils import check_callback_rules, check_session_state_rules
class TimeWidgetsMixin:
def time_input(
self,
label: str,
value=None,
key: Optional[Key] = None,
help: Optional[str] = None,
on_change: Optional[WidgetCallback] = None,
args: Optional[WidgetArgs] = None,
kwargs: Optional[WidgetKwargs] = None,
*, # keyword-only arguments:
disabled: bool = False,
) -> time:
"""Display a time input widget.
Parameters
----------
label : str
A short label explaining to the user what this time input is for.
value : datetime.time/datetime.datetime
The value of this widget when it first renders. This will be
cast to str internally. Defaults to the current time.
key : str or int
An optional string or integer to use as the unique key for the widget.
If this is omitted, a key will be generated for the widget
based on its content. Multiple widgets of the same type may
not share the same key.
help : str
An optional tooltip that gets displayed next to the input.
on_change : callable
An optional callback invoked when this time_input's value changes.
args : tuple
An optional tuple of args to pass to the callback.
kwargs : dict
An optional dict of kwargs to pass to the callback.
disabled : bool
An optional boolean, which disables the time input if set to True.
The default is False. This argument can only be supplied by keyword.
Returns
-------
datetime.time
The current value of the time input widget.
Example
-------
>>> t = st.time_input('Set an alarm for', datetime.time(8, 45))
>>> st.write('Alarm is set for', t)
.. output::
https://share.streamlit.io/streamlit/docs/main/python/api-examples-source/widget.time_input.py
height: 260px
"""
ctx = get_script_run_ctx()
return self._time_input(
label=label,
value=value,
key=key,
help=help,
on_change=on_change,
args=args,
kwargs=kwargs,
disabled=disabled,
ctx=ctx,
)
def _time_input(
self,
label: str,
value=None,
key: Optional[Key] = None,
help: Optional[str] = None,
on_change: Optional[WidgetCallback] = None,
args: Optional[WidgetArgs] = None,
kwargs: Optional[WidgetKwargs] = None,
*, # keyword-only arguments:
disabled: bool = False,
ctx: Optional[ScriptRunContext] = None,
) -> time:
key = to_key(key)
check_callback_rules(self.dg, on_change)
check_session_state_rules(default_value=value, key=key)
# Set value default.
if value is None:
value = datetime.now().time().replace(second=0, microsecond=0)
# Ensure that the value is either datetime/time
if not isinstance(value, datetime) and not isinstance(value, time):
raise StreamlitAPIException(
"The type of the value should be either datetime or time."
)
# Convert datetime to time
if isinstance(value, datetime):
value = value.time().replace(second=0, microsecond=0)
time_input_proto = TimeInputProto()
time_input_proto.label = label
time_input_proto.default = time.strftime(value, "%H:%M")
time_input_proto.form_id = current_form_id(self.dg)
if help is not None:
time_input_proto.help = dedent(help)
def deserialize_time_input(ui_value, widget_id=""):
return (
datetime.strptime(ui_value, "%H:%M").time()
if ui_value is not None
else value
)
def serialize_time_input(v):
if isinstance(v, datetime):
v = v.time()
return time.strftime(v, "%H:%M")
current_value, set_frontend_value = register_widget(
"time_input",
time_input_proto,
user_key=key,
on_change_handler=on_change,
args=args,
kwargs=kwargs,
deserializer=deserialize_time_input,
serializer=serialize_time_input,
ctx=ctx,
)
# This needs to be done after register_widget because we don't want
# the following proto fields to affect a widget's ID.
time_input_proto.disabled = disabled
if set_frontend_value:
time_input_proto.value = serialize_time_input(current_value)
time_input_proto.set_value = True
self.dg._enqueue("time_input", time_input_proto)
return cast(time, current_value)
def date_input(
self,
label: str,
value=None,
min_value=None,
max_value=None,
key: Optional[Key] = None,
help: Optional[str] = None,
on_change: Optional[WidgetCallback] = None,
args: Optional[WidgetArgs] = None,
kwargs: Optional[WidgetKwargs] = None,
*, # keyword-only arguments:
disabled: bool = False,
) -> Union[date, Tuple[date, ...]]:
"""Display a date input widget.
Parameters
----------
label : str
A short label explaining to the user what this date input is for.
value : datetime.date or datetime.datetime or list/tuple of datetime.date or datetime.datetime or None
The value of this widget when it first renders. If a list/tuple with
0 to 2 date/datetime values is provided, the datepicker will allow
users to provide a range. Defaults to today as a single-date picker.
min_value : datetime.date or datetime.datetime
The minimum selectable date. If value is a date, defaults to value - 10 years.
If value is the interval [start, end], defaults to start - 10 years.
max_value : datetime.date or datetime.datetime
The maximum selectable date. If value is a date, defaults to value + 10 years.
If value is the interval [start, end], defaults to end + 10 years.
key : str or int
An optional string or integer to use as the unique key for the widget.
If this is omitted, a key will be generated for the widget
based on its content. Multiple widgets of the same type may
not share the same key.
help : str
An optional tooltip that gets displayed next to the input.
on_change : callable
An optional callback invoked when this date_input's value changes.
args : tuple
An optional tuple of args to pass to the callback.
kwargs : dict
An optional dict of kwargs to pass to the callback.
disabled : bool
An optional boolean, which disables the date input if set to True.
The default is False. This argument can only be supplied by keyword.
Returns
-------
datetime.date or a tuple with 0-2 dates
The current value of the date input widget.
Example
-------
>>> d = st.date_input(
... "When\'s your birthday",
... datetime.date(2019, 7, 6))
>>> st.write('Your birthday is:', d)
.. output::
https://share.streamlit.io/streamlit/docs/main/python/api-examples-source/widget.date_input.py
height: 260px
"""
ctx = get_script_run_ctx()
return self._date_input(
label=label,
value=value,
min_value=min_value,
max_value=max_value,
key=key,
help=help,
on_change=on_change,
args=args,
kwargs=kwargs,
disabled=disabled,
ctx=ctx,
)
def _date_input(
self,
label: str,
value=None,
min_value=None,
max_value=None,
key: Optional[Key] = None,
help: Optional[str] = None,
on_change: Optional[WidgetCallback] = None,
args: Optional[WidgetArgs] = None,
kwargs: Optional[WidgetKwargs] = None,
*, # keyword-only arguments:
disabled: bool = False,
ctx: Optional[ScriptRunContext] = None,
) -> Union[date, Tuple[date, ...]]:
key = to_key(key)
check_callback_rules(self.dg, on_change)
check_session_state_rules(default_value=value, key=key)
# Set value default.
if value is None:
value = datetime.now().date()
single_value = isinstance(value, (date, datetime))
range_value = isinstance(value, (list, tuple)) and len(value) in (0, 1, 2)
if not single_value and not range_value:
raise StreamlitAPIException(
"DateInput value should either be an date/datetime or a list/tuple of "
"0 - 2 date/datetime values"
)
if single_value:
value = [value]
value = [v.date() if isinstance(v, datetime) else v for v in value]
if isinstance(min_value, datetime):
min_value = min_value.date()
elif min_value is None:
if value:
min_value = value[0] - relativedelta.relativedelta(years=10)
else:
min_value = date.today() - relativedelta.relativedelta(years=10)
if isinstance(max_value, datetime):
max_value = max_value.date()
elif max_value is None:
if value:
max_value = value[-1] + relativedelta.relativedelta(years=10)
else:
max_value = date.today() + relativedelta.relativedelta(years=10)
if value:
start_value = value[0]
end_value = value[-1]
if (start_value < min_value) or (end_value > max_value):
raise StreamlitAPIException(
f"The default `value` of {value} "
f"must lie between the `min_value` of {min_value} "
f"and the `max_value` of {max_value}, inclusively."
)
date_input_proto = DateInputProto()
date_input_proto.is_range = range_value
if help is not None:
date_input_proto.help = dedent(help)
date_input_proto.label = label
date_input_proto.default[:] = [date.strftime(v, "%Y/%m/%d") for v in value]
date_input_proto.min = date.strftime(min_value, "%Y/%m/%d")
date_input_proto.max = date.strftime(max_value, "%Y/%m/%d")
date_input_proto.form_id = current_form_id(self.dg)
def deserialize_date_input(ui_value, widget_id=""):
if ui_value is not None:
return_value = [
datetime.strptime(v, "%Y/%m/%d").date() for v in ui_value
]
else:
return_value = value
return return_value[0] if single_value else tuple(return_value)
def serialize_date_input(v):
range_value = isinstance(v, (list, tuple))
to_serialize = list(v) if range_value else [v]
return [date.strftime(v, "%Y/%m/%d") for v in to_serialize]
current_value, set_frontend_value = register_widget(
"date_input",
date_input_proto,
user_key=key,
on_change_handler=on_change,
args=args,
kwargs=kwargs,
deserializer=deserialize_date_input,
serializer=serialize_date_input,
ctx=ctx,
)
# This needs to be done after register_widget because we don't want
# the following proto fields to affect a widget's ID.
date_input_proto.disabled = disabled
if set_frontend_value:
date_input_proto.value[:] = serialize_date_input(current_value)
date_input_proto.set_value = True
self.dg._enqueue("date_input", date_input_proto)
return cast(date, current_value)
@property
def dg(self) -> "streamlit.delta_generator.DeltaGenerator":
"""Get our DeltaGenerator."""
return cast("streamlit.delta_generator.DeltaGenerator", self)

View File

@@ -0,0 +1,84 @@
# Copyright 2018-2022 Streamlit Inc.
#
# 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.
import textwrap
from typing import Any, Optional, TYPE_CHECKING
import streamlit
from streamlit import type_util
from streamlit.elements.form import is_in_form
from streamlit.errors import StreamlitAPIException
from streamlit.state import get_session_state, WidgetCallback
if TYPE_CHECKING:
from streamlit.delta_generator import DeltaGenerator
def clean_text(text: Any) -> str:
"""Convert an object to text, dedent it, and strip whitespace."""
return textwrap.dedent(str(text)).strip()
def last_index_for_melted_dataframes(data):
if type_util.is_dataframe_compatible(data):
data = type_util.convert_anything_to_df(data)
if data.index.size > 0:
return data.index[-1]
return None
def check_callback_rules(
dg: "DeltaGenerator", on_change: Optional[WidgetCallback]
) -> None:
if (
streamlit._is_running_with_streamlit
and is_in_form(dg)
and on_change is not None
):
raise StreamlitAPIException(
"With forms, callbacks can only be defined on the `st.form_submit_button`."
" Defining callbacks on other widgets inside a form is not allowed."
)
_shown_default_value_warning = False
def check_session_state_rules(
default_value: Any, key: Optional[str], writes_allowed: bool = True
) -> None:
global _shown_default_value_warning
if key is None or not streamlit._is_running_with_streamlit:
return
session_state = get_session_state()
if not session_state.is_new_state_value(key):
return
if not writes_allowed:
raise StreamlitAPIException(
"Values for st.button, st.download_button, st.file_uploader, and "
"st.form cannot be set using st.session_state."
)
if default_value is not None and not _shown_default_value_warning:
streamlit.warning(
f'The widget with key "{key}" was created with a default value but'
" also had its value set via the Session State API."
)
_shown_default_value_warning = True

View File

@@ -0,0 +1,239 @@
# Copyright 2018-2022 Streamlit Inc.
#
# 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.
import inspect
import json as json
import types
from typing import cast, Any, List, Tuple, Type
import numpy as np
import streamlit
from streamlit import type_util
from streamlit.errors import StreamlitAPIException
from streamlit.state import SessionStateProxy
# Special methods:
HELP_TYPES = (
types.BuiltinFunctionType,
types.BuiltinMethodType,
types.FunctionType,
types.MethodType,
types.ModuleType,
) # type: Tuple[Type[Any], ...]
class WriteMixin:
def write(self, *args, **kwargs):
"""Write arguments to the app.
This is the Swiss Army knife of Streamlit commands: it does different
things depending on what you throw at it. Unlike other Streamlit commands,
write() has some unique properties:
1. You can pass in multiple arguments, all of which will be written.
2. Its behavior depends on the input types as follows.
3. It returns None, so its "slot" in the App cannot be reused.
Parameters
----------
*args : any
One or many objects to print to the App.
Arguments are handled as follows:
- write(string) : Prints the formatted Markdown string, with
support for LaTeX expression and emoji shortcodes.
See docs for st.markdown for more.
- write(data_frame) : Displays the DataFrame as a table.
- write(error) : Prints an exception specially.
- write(func) : Displays information about a function.
- write(module) : Displays information about the module.
- write(dict) : Displays dict in an interactive widget.
- write(mpl_fig) : Displays a Matplotlib figure.
- write(altair) : Displays an Altair chart.
- write(keras) : Displays a Keras model.
- write(graphviz) : Displays a Graphviz graph.
- write(plotly_fig) : Displays a Plotly figure.
- write(bokeh_fig) : Displays a Bokeh figure.
- write(sympy_expr) : Prints SymPy expression using LaTeX.
- write(htmlable) : Prints _repr_html_() for the object if available.
- write(obj) : Prints str(obj) if otherwise unknown.
unsafe_allow_html : bool
This is a keyword-only argument that defaults to False.
By default, any HTML tags found in strings will be escaped and
therefore treated as pure text. This behavior may be turned off by
setting this argument to True.
That said, *we strongly advise against it*. It is hard to write secure
HTML, so by using this argument you may be compromising your users'
security. For more information, see:
https://github.com/streamlit/streamlit/issues/152
**Also note that `unsafe_allow_html` is a temporary measure and may be
removed from Streamlit at any time.**
If you decide to turn on HTML anyway, we ask you to please tell us your
exact use case here:
https://discuss.streamlit.io/t/96 .
This will help us come up with safe APIs that allow you to do what you
want.
Example
-------
Its basic use case is to draw Markdown-formatted text, whenever the
input is a string:
>>> write('Hello, *World!* :sunglasses:')
.. output::
https://share.streamlit.io/streamlit/docs/main/python/api-examples-source/text.write1.py
height: 150px
As mentioned earlier, `st.write()` also accepts other data formats, such as
numbers, data frames, styled data frames, and assorted objects:
>>> st.write(1234)
>>> st.write(pd.DataFrame({
... 'first column': [1, 2, 3, 4],
... 'second column': [10, 20, 30, 40],
... }))
.. output::
https://share.streamlit.io/streamlit/docs/main/python/api-examples-source/text.write2.py
height: 350px
Finally, you can pass in multiple arguments to do things like:
>>> st.write('1 + 1 = ', 2)
>>> st.write('Below is a DataFrame:', data_frame, 'Above is a dataframe.')
.. output::
https://share.streamlit.io/streamlit/docs/main/python/api-examples-source/text.write3.py
height: 410px
Oh, one more thing: `st.write` accepts chart objects too! For example:
>>> import pandas as pd
>>> import numpy as np
>>> import altair as alt
>>>
>>> df = pd.DataFrame(
... np.random.randn(200, 3),
... columns=['a', 'b', 'c'])
...
>>> c = alt.Chart(df).mark_circle().encode(
... x='a', y='b', size='c', color='c', tooltip=['a', 'b', 'c'])
>>>
>>> st.write(c)
.. output::
https://share.streamlit.io/streamlit/docs/main/python/api-examples-source/charts.vega_lite_chart.py
height: 300px
"""
string_buffer = [] # type: List[str]
unsafe_allow_html = kwargs.get("unsafe_allow_html", False)
# This bans some valid cases like: e = st.empty(); e.write("a", "b").
# BUT: 1) such cases are rare, 2) this rule is easy to understand,
# and 3) this rule should be removed once we have st.container()
if not self.dg._is_top_level and len(args) > 1:
raise StreamlitAPIException(
"Cannot replace a single element with multiple elements.\n\n"
"The `write()` method only supports multiple elements when "
"inserting elements rather than replacing. That is, only "
"when called as `st.write()` or `st.sidebar.write()`."
)
def flush_buffer():
if string_buffer:
self.dg.markdown(
" ".join(string_buffer),
unsafe_allow_html=unsafe_allow_html,
)
string_buffer[:] = []
for arg in args:
# Order matters!
if isinstance(arg, str):
string_buffer.append(arg)
elif type_util.is_dataframe_like(arg):
flush_buffer()
if len(np.shape(arg)) > 2:
self.dg.text(arg)
else:
self.dg.dataframe(arg)
elif isinstance(arg, Exception):
flush_buffer()
self.dg.exception(arg)
elif isinstance(arg, HELP_TYPES):
flush_buffer()
self.dg.help(arg)
elif type_util.is_altair_chart(arg):
flush_buffer()
self.dg.altair_chart(arg)
elif type_util.is_type(arg, "matplotlib.figure.Figure"):
flush_buffer()
self.dg.pyplot(arg)
elif type_util.is_plotly_chart(arg):
flush_buffer()
self.dg.plotly_chart(arg)
elif type_util.is_type(arg, "bokeh.plotting.figure.Figure"):
flush_buffer()
self.dg.bokeh_chart(arg)
elif type_util.is_graphviz_chart(arg):
flush_buffer()
self.dg.graphviz_chart(arg)
elif type_util.is_sympy_expession(arg):
flush_buffer()
self.dg.latex(arg)
elif type_util.is_keras_model(arg):
from tensorflow.python.keras.utils import vis_utils
flush_buffer()
dot = vis_utils.model_to_dot(arg)
self.dg.graphviz_chart(dot.to_string())
elif isinstance(arg, (dict, list, SessionStateProxy)):
flush_buffer()
self.dg.json(arg)
elif type_util.is_namedtuple(arg):
flush_buffer()
self.dg.json(json.dumps(arg._asdict()))
elif type_util.is_pydeck(arg):
flush_buffer()
self.dg.pydeck_chart(arg)
elif inspect.isclass(arg):
flush_buffer()
self.dg.text(arg)
elif hasattr(arg, "_repr_html_"):
self.dg.markdown(
arg._repr_html_(),
unsafe_allow_html=True,
)
else:
string_buffer.append("`%s`" % str(arg).replace("`", "\\`"))
flush_buffer()
@property
def dg(self) -> "streamlit.delta_generator.DeltaGenerator":
"""Get our DeltaGenerator."""
return cast("streamlit.delta_generator.DeltaGenerator", self)