streamlit-nightly 1.33.1.dev20240501__py2.py3-none-any.whl → 1.34.1.dev20240503__py2.py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- streamlit/components/v1/custom_component.py +3 -9
- streamlit/delta_generator.py +32 -208
- streamlit/elements/lib/built_in_chart_utils.py +920 -0
- streamlit/elements/utils.py +1 -14
- streamlit/elements/{arrow_altair.py → vega_charts.py} +301 -836
- streamlit/static/asset-manifest.json +5 -5
- streamlit/static/index.html +1 -1
- streamlit/static/static/js/5441.71804c26.chunk.js +1 -0
- streamlit/static/static/js/7483.64f23be7.chunk.js +2 -0
- streamlit/static/static/js/{main.af77b7ba.js → main.3b0201f6.js} +2 -2
- {streamlit_nightly-1.33.1.dev20240501.dist-info → streamlit_nightly-1.34.1.dev20240503.dist-info}/METADATA +1 -1
- {streamlit_nightly-1.33.1.dev20240501.dist-info → streamlit_nightly-1.34.1.dev20240503.dist-info}/RECORD +19 -20
- streamlit/elements/altair_utils.py +0 -40
- streamlit/elements/arrow_vega_lite.py +0 -229
- streamlit/static/static/js/43.c6749504.chunk.js +0 -1
- streamlit/static/static/js/656.7150a933.chunk.js +0 -2
- /streamlit/static/static/css/{43.e3b876c5.chunk.css → 5441.e3b876c5.chunk.css} +0 -0
- /streamlit/static/static/js/{656.7150a933.chunk.js.LICENSE.txt → 7483.64f23be7.chunk.js.LICENSE.txt} +0 -0
- /streamlit/static/static/js/{main.af77b7ba.js.LICENSE.txt → main.3b0201f6.js.LICENSE.txt} +0 -0
- {streamlit_nightly-1.33.1.dev20240501.data → streamlit_nightly-1.34.1.dev20240503.data}/scripts/streamlit.cmd +0 -0
- {streamlit_nightly-1.33.1.dev20240501.dist-info → streamlit_nightly-1.34.1.dev20240503.dist-info}/WHEEL +0 -0
- {streamlit_nightly-1.33.1.dev20240501.dist-info → streamlit_nightly-1.34.1.dev20240503.dist-info}/entry_points.txt +0 -0
- {streamlit_nightly-1.33.1.dev20240501.dist-info → streamlit_nightly-1.34.1.dev20240503.dist-info}/top_level.txt +0 -0
@@ -12,30 +12,22 @@
|
|
12
12
|
# See the License for the specific language governing permissions and
|
13
13
|
# limitations under the License.
|
14
14
|
|
15
|
-
"""
|
16
|
-
|
17
|
-
a nice JSON schema for expressing graphs and charts.
|
18
|
-
"""
|
15
|
+
"""Collection of chart commands that are rendered via our vega-lite chart component."""
|
16
|
+
|
19
17
|
from __future__ import annotations
|
20
18
|
|
19
|
+
import json
|
21
20
|
from contextlib import nullcontext
|
22
|
-
from
|
23
|
-
from enum import Enum
|
24
|
-
from typing import TYPE_CHECKING, Any, Collection, Literal, Sequence, cast
|
21
|
+
from typing import TYPE_CHECKING, Any, Final, Literal, Sequence, cast
|
25
22
|
|
26
|
-
import streamlit.elements.
|
23
|
+
import streamlit.elements.lib.dicttools as dicttools
|
27
24
|
from streamlit import type_util
|
28
|
-
from streamlit.
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
is_hex_color_like,
|
33
|
-
to_css_color,
|
25
|
+
from streamlit.elements.lib.built_in_chart_utils import (
|
26
|
+
AddRowsMetadata,
|
27
|
+
ChartType,
|
28
|
+
generate_chart,
|
34
29
|
)
|
35
|
-
from streamlit.
|
36
|
-
from streamlit.elements.arrow import Data
|
37
|
-
from streamlit.elements.utils import last_index_for_melted_dataframes
|
38
|
-
from streamlit.errors import Error, StreamlitAPIException
|
30
|
+
from streamlit.errors import StreamlitAPIException
|
39
31
|
from streamlit.proto.ArrowVegaLiteChart_pb2 import (
|
40
32
|
ArrowVegaLiteChart as ArrowVegaLiteChartProto,
|
41
33
|
)
|
@@ -43,54 +35,171 @@ from streamlit.runtime.metrics_util import gather_metrics
|
|
43
35
|
|
44
36
|
if TYPE_CHECKING:
|
45
37
|
import altair as alt
|
46
|
-
import pandas as pd
|
47
38
|
|
39
|
+
from streamlit.color_util import Color
|
48
40
|
from streamlit.delta_generator import DeltaGenerator
|
41
|
+
from streamlit.elements.arrow import Data
|
42
|
+
|
43
|
+
# See https://vega.github.io/vega-lite/docs/encoding.html
|
44
|
+
_CHANNELS: Final = {
|
45
|
+
"x",
|
46
|
+
"y",
|
47
|
+
"x2",
|
48
|
+
"y2",
|
49
|
+
"xError",
|
50
|
+
"xError2",
|
51
|
+
"yError",
|
52
|
+
"yError2",
|
53
|
+
"longitude",
|
54
|
+
"latitude",
|
55
|
+
"color",
|
56
|
+
"opacity",
|
57
|
+
"fillOpacity",
|
58
|
+
"strokeOpacity",
|
59
|
+
"strokeWidth",
|
60
|
+
"size",
|
61
|
+
"shape",
|
62
|
+
"text",
|
63
|
+
"tooltip",
|
64
|
+
"href",
|
65
|
+
"key",
|
66
|
+
"order",
|
67
|
+
"detail",
|
68
|
+
"facet",
|
69
|
+
"row",
|
70
|
+
"column",
|
71
|
+
}
|
72
|
+
|
73
|
+
|
74
|
+
def _prepare_vega_lite_spec(
|
75
|
+
spec: dict[str, Any] | None = None,
|
76
|
+
use_container_width: bool = False,
|
77
|
+
**kwargs,
|
78
|
+
) -> dict[str, Any]:
|
79
|
+
# Support passing no spec arg, but filling it with kwargs.
|
80
|
+
# Example:
|
81
|
+
# marshall(proto, baz='boz')
|
82
|
+
if spec is None:
|
83
|
+
spec = dict()
|
84
|
+
|
85
|
+
if len(kwargs):
|
86
|
+
# Support passing in kwargs. Example:
|
87
|
+
# marshall(proto, {foo: 'bar'}, baz='boz')
|
88
|
+
# Merge spec with unflattened kwargs, where kwargs take precedence.
|
89
|
+
# This only works for string keys, but kwarg keys are strings anyways.
|
90
|
+
spec = dict(spec, **dicttools.unflatten(kwargs, _CHANNELS))
|
91
|
+
else:
|
92
|
+
# Clone the spec dict, since we may be mutating it.
|
93
|
+
spec = dict(spec)
|
94
|
+
|
95
|
+
if len(spec) == 0:
|
96
|
+
raise StreamlitAPIException("Vega-Lite charts require a non-empty spec dict.")
|
97
|
+
|
98
|
+
if "autosize" not in spec:
|
99
|
+
# type fit does not work for many chart types. This change focuses
|
100
|
+
# on vconcat with use_container_width=True as there are unintended
|
101
|
+
# consequences of changing the default autosize for all charts.
|
102
|
+
# fit-x fits the width and height can be adjusted.
|
103
|
+
if "vconcat" in spec and use_container_width:
|
104
|
+
spec["autosize"] = {"type": "fit-x", "contains": "padding"}
|
105
|
+
else:
|
106
|
+
spec["autosize"] = {"type": "fit", "contains": "padding"}
|
49
107
|
|
108
|
+
return spec
|
50
109
|
|
51
|
-
class ChartType(Enum):
|
52
|
-
AREA = {"mark_type": "area"}
|
53
|
-
BAR = {"mark_type": "bar"}
|
54
|
-
LINE = {"mark_type": "line"}
|
55
|
-
SCATTER = {"mark_type": "circle"}
|
56
110
|
|
111
|
+
def _serialize_data(data: Any) -> bytes:
|
112
|
+
"""Serialize the any type of data structure to Arrow IPC format (bytes)."""
|
113
|
+
import pyarrow as pa
|
114
|
+
|
115
|
+
if isinstance(data, pa.Table):
|
116
|
+
return type_util.pyarrow_table_to_bytes(data)
|
117
|
+
|
118
|
+
df = type_util.convert_anything_to_df(data)
|
119
|
+
return type_util.data_frame_to_bytes(df)
|
120
|
+
|
121
|
+
|
122
|
+
def _marshall_chart_data(
|
123
|
+
proto: ArrowVegaLiteChartProto,
|
124
|
+
spec: dict[str, Any],
|
125
|
+
data: Data = None,
|
126
|
+
) -> None:
|
127
|
+
"""Adds the data to the proto and removes it from the spec dict.
|
128
|
+
These operations will happen in-place."""
|
129
|
+
|
130
|
+
# Pull data out of spec dict when it's in a 'datasets' key:
|
131
|
+
# datasets: {foo: df1, bar: df2}, ...}
|
132
|
+
if "datasets" in spec:
|
133
|
+
for dataset_name, dataset_data in spec["datasets"].items():
|
134
|
+
dataset = proto.datasets.add()
|
135
|
+
dataset.name = str(dataset_name)
|
136
|
+
dataset.has_name = True
|
137
|
+
dataset.data.data = _serialize_data(dataset_data)
|
138
|
+
del spec["datasets"]
|
139
|
+
|
140
|
+
# Pull data out of spec dict when it's in a top-level 'data' key:
|
141
|
+
# {data: df}
|
142
|
+
# {data: {values: df, ...}}
|
143
|
+
# {data: {url: 'url'}}
|
144
|
+
# {data: {name: 'foo'}}
|
145
|
+
if "data" in spec:
|
146
|
+
data_spec = spec["data"]
|
147
|
+
|
148
|
+
if isinstance(data_spec, dict):
|
149
|
+
if "values" in data_spec:
|
150
|
+
data = data_spec["values"]
|
151
|
+
del spec["data"]
|
152
|
+
else:
|
153
|
+
data = data_spec
|
154
|
+
del spec["data"]
|
155
|
+
|
156
|
+
if data is not None:
|
157
|
+
proto.data.data = _serialize_data(data)
|
158
|
+
|
159
|
+
|
160
|
+
def _convert_altair_to_vega_lite_spec(altair_chart: alt.Chart) -> dict[str, Any]:
|
161
|
+
"""Convert an Altair chart object to a Vega-Lite chart spec."""
|
162
|
+
import altair as alt
|
163
|
+
|
164
|
+
# Normally altair_chart.to_dict() would transform the dataframe used by the
|
165
|
+
# chart into an array of dictionaries. To avoid that, we install a
|
166
|
+
# transformer that replaces datasets with a reference by the object id of
|
167
|
+
# the dataframe. We then fill in the dataset manually later on.
|
168
|
+
|
169
|
+
datasets = {}
|
170
|
+
|
171
|
+
def id_transform(data) -> dict[str, str]:
|
172
|
+
"""Altair data transformer that returns a fake named dataset with the
|
173
|
+
object id.
|
174
|
+
"""
|
175
|
+
name = str(id(data))
|
176
|
+
datasets[name] = data
|
177
|
+
return {"name": name}
|
178
|
+
|
179
|
+
alt.data_transformers.register("id", id_transform) # type: ignore[attr-defined,unused-ignore]
|
180
|
+
|
181
|
+
# The default altair theme has some width/height defaults defined
|
182
|
+
# which are not useful for Streamlit. Therefore, we change the theme to
|
183
|
+
# "none" to avoid those defaults.
|
184
|
+
with alt.themes.enable("none") if alt.themes.active == "default" else nullcontext(): # type: ignore[attr-defined,unused-ignore]
|
185
|
+
with alt.data_transformers.enable("id"): # type: ignore[attr-defined,unused-ignore]
|
186
|
+
chart_dict = altair_chart.to_dict()
|
187
|
+
|
188
|
+
# Put datasets back into the chart dict but note how they weren't
|
189
|
+
# transformed.
|
190
|
+
chart_dict["datasets"] = datasets
|
191
|
+
return chart_dict
|
192
|
+
|
193
|
+
|
194
|
+
class VegaChartsMixin:
|
195
|
+
"""Mix-in class for all vega-related chart commands.
|
196
|
+
|
197
|
+
Altair is a python wrapper on top of the vega-lite spec. And our
|
198
|
+
built-in chart commands are just another layer on-top of Altair.
|
199
|
+
All of these chart commands will be eventually converted to a vega-lite
|
200
|
+
spec and rendered using the same vega-lite chart component.
|
201
|
+
"""
|
57
202
|
|
58
|
-
# Color and size legends need different title paddings in order for them
|
59
|
-
# to be vertically aligned.
|
60
|
-
#
|
61
|
-
# NOTE: I don't think it's possible to *perfectly* align the size and
|
62
|
-
# color legends in all instances, since the "size" circles vary in size based
|
63
|
-
# on the data, and their container is top-aligned with the color container. But
|
64
|
-
# through trial-and-error I found this value to be a good enough middle ground.
|
65
|
-
# See e2e/scripts/st_arrow_scatter_chart.py for some alignment tests.
|
66
|
-
#
|
67
|
-
# NOTE #2: In theory, we could move COLOR_LEGEND_SETTINGS into
|
68
|
-
# ArrowVegaLiteChart/CustomTheme.tsx, but this would impact existing behavior.
|
69
|
-
# (See https://github.com/streamlit/streamlit/pull/7164#discussion_r1307707345)
|
70
|
-
COLOR_LEGEND_SETTINGS = dict(titlePadding=5, offset=5, orient="bottom")
|
71
|
-
SIZE_LEGEND_SETTINGS = dict(titlePadding=0.5, offset=5, orient="bottom")
|
72
|
-
|
73
|
-
# User-readable names to give the index and melted columns.
|
74
|
-
SEPARATED_INDEX_COLUMN_TITLE = "index"
|
75
|
-
MELTED_Y_COLUMN_TITLE = "value"
|
76
|
-
MELTED_COLOR_COLUMN_TITLE = "color"
|
77
|
-
|
78
|
-
# Crazy internal (non-user-visible) names for the index and melted columns, in order to
|
79
|
-
# avoid collision with existing column names. The suffix below was generated with an
|
80
|
-
# online random number generator. Rationale: because it makes it even less likely to
|
81
|
-
# lead to a conflict than something that's human-readable (like "--streamlit-fake-field"
|
82
|
-
# or something).
|
83
|
-
PROTECTION_SUFFIX = "--p5bJXXpQgvPz6yvQMFiy"
|
84
|
-
SEPARATED_INDEX_COLUMN_NAME = SEPARATED_INDEX_COLUMN_TITLE + PROTECTION_SUFFIX
|
85
|
-
MELTED_Y_COLUMN_NAME = MELTED_Y_COLUMN_TITLE + PROTECTION_SUFFIX
|
86
|
-
MELTED_COLOR_COLUMN_NAME = MELTED_COLOR_COLUMN_TITLE + PROTECTION_SUFFIX
|
87
|
-
|
88
|
-
# Name we use for a column we know doesn't exist in the data, to address a Vega-Lite rendering bug
|
89
|
-
# where empty charts need x, y encodings set in order to take up space.
|
90
|
-
NON_EXISTENT_COLUMN_NAME = "DOES_NOT_EXIST" + PROTECTION_SUFFIX
|
91
|
-
|
92
|
-
|
93
|
-
class ArrowAltairMixin:
|
94
203
|
@gather_metrics("line_chart")
|
95
204
|
def line_chart(
|
96
205
|
self,
|
@@ -232,8 +341,8 @@ class ArrowAltairMixin:
|
|
232
341
|
height: 440px
|
233
342
|
|
234
343
|
"""
|
235
|
-
|
236
|
-
chart, add_rows_metadata =
|
344
|
+
|
345
|
+
chart, add_rows_metadata = generate_chart(
|
237
346
|
chart_type=ChartType.LINE,
|
238
347
|
data=data,
|
239
348
|
x_from_user=x,
|
@@ -243,10 +352,11 @@ class ArrowAltairMixin:
|
|
243
352
|
width=width,
|
244
353
|
height=height,
|
245
354
|
)
|
246
|
-
|
247
|
-
|
248
|
-
|
249
|
-
"
|
355
|
+
return self._altair_chart(
|
356
|
+
chart,
|
357
|
+
use_container_width=use_container_width,
|
358
|
+
theme="streamlit",
|
359
|
+
add_rows_metadata=add_rows_metadata,
|
250
360
|
)
|
251
361
|
|
252
362
|
@gather_metrics("area_chart")
|
@@ -391,8 +501,7 @@ class ArrowAltairMixin:
|
|
391
501
|
|
392
502
|
"""
|
393
503
|
|
394
|
-
|
395
|
-
chart, add_rows_metadata = _generate_chart(
|
504
|
+
chart, add_rows_metadata = generate_chart(
|
396
505
|
chart_type=ChartType.AREA,
|
397
506
|
data=data,
|
398
507
|
x_from_user=x,
|
@@ -402,10 +511,11 @@ class ArrowAltairMixin:
|
|
402
511
|
width=width,
|
403
512
|
height=height,
|
404
513
|
)
|
405
|
-
|
406
|
-
|
407
|
-
|
408
|
-
"
|
514
|
+
return self._altair_chart(
|
515
|
+
chart,
|
516
|
+
use_container_width=use_container_width,
|
517
|
+
theme="streamlit",
|
518
|
+
add_rows_metadata=add_rows_metadata,
|
409
519
|
)
|
410
520
|
|
411
521
|
@gather_metrics("bar_chart")
|
@@ -552,8 +662,7 @@ class ArrowAltairMixin:
|
|
552
662
|
|
553
663
|
"""
|
554
664
|
|
555
|
-
|
556
|
-
chart, add_rows_metadata = _generate_chart(
|
665
|
+
chart, add_rows_metadata = generate_chart(
|
557
666
|
chart_type=ChartType.BAR,
|
558
667
|
data=data,
|
559
668
|
x_from_user=x,
|
@@ -563,10 +672,11 @@ class ArrowAltairMixin:
|
|
563
672
|
width=width,
|
564
673
|
height=height,
|
565
674
|
)
|
566
|
-
|
567
|
-
|
568
|
-
|
569
|
-
"
|
675
|
+
return self._altair_chart(
|
676
|
+
chart,
|
677
|
+
use_container_width=use_container_width,
|
678
|
+
theme="streamlit",
|
679
|
+
add_rows_metadata=add_rows_metadata,
|
570
680
|
)
|
571
681
|
|
572
682
|
@gather_metrics("scatter_chart")
|
@@ -725,8 +835,8 @@ class ArrowAltairMixin:
|
|
725
835
|
height: 440px
|
726
836
|
|
727
837
|
"""
|
728
|
-
|
729
|
-
chart, add_rows_metadata =
|
838
|
+
|
839
|
+
chart, add_rows_metadata = generate_chart(
|
730
840
|
chart_type=ChartType.SCATTER,
|
731
841
|
data=data,
|
732
842
|
x_from_user=x,
|
@@ -736,10 +846,11 @@ class ArrowAltairMixin:
|
|
736
846
|
width=width,
|
737
847
|
height=height,
|
738
848
|
)
|
739
|
-
|
740
|
-
|
741
|
-
|
742
|
-
"
|
849
|
+
return self._altair_chart(
|
850
|
+
chart,
|
851
|
+
use_container_width=use_container_width,
|
852
|
+
theme="streamlit",
|
853
|
+
add_rows_metadata=add_rows_metadata,
|
743
854
|
)
|
744
855
|
|
745
856
|
@gather_metrics("altair_chart")
|
@@ -790,780 +901,134 @@ class ArrowAltairMixin:
|
|
790
901
|
https://altair-viz.github.io/gallery/.
|
791
902
|
|
792
903
|
"""
|
793
|
-
|
794
|
-
|
795
|
-
f'You set theme="{theme}" while Streamlit charts only support theme=”streamlit” or theme=None to fallback to the default library theme.'
|
796
|
-
)
|
797
|
-
proto = ArrowVegaLiteChartProto()
|
798
|
-
marshall(
|
799
|
-
proto,
|
800
|
-
altair_chart,
|
801
|
-
use_container_width=use_container_width,
|
802
|
-
theme=theme,
|
803
|
-
)
|
804
|
-
|
805
|
-
return self.dg._enqueue("arrow_vega_lite_chart", proto)
|
806
|
-
|
807
|
-
@property
|
808
|
-
def dg(self) -> DeltaGenerator:
|
809
|
-
"""Get our DeltaGenerator."""
|
810
|
-
return cast("DeltaGenerator", self)
|
811
|
-
|
812
|
-
|
813
|
-
def _is_date_column(df: pd.DataFrame, name: str | None) -> bool:
|
814
|
-
"""True if the column with the given name stores datetime.date values.
|
815
|
-
|
816
|
-
This function just checks the first value in the given column, so
|
817
|
-
it's meaningful only for columns whose values all share the same type.
|
818
|
-
|
819
|
-
Parameters
|
820
|
-
----------
|
821
|
-
df : pd.DataFrame
|
822
|
-
name : str
|
823
|
-
The column name
|
824
|
-
|
825
|
-
Returns
|
826
|
-
-------
|
827
|
-
bool
|
828
|
-
|
829
|
-
"""
|
830
|
-
if name is None:
|
831
|
-
return False
|
832
|
-
|
833
|
-
column = df[name]
|
834
|
-
if column.size == 0:
|
835
|
-
return False
|
836
|
-
|
837
|
-
return isinstance(column.iloc[0], date)
|
838
|
-
|
839
|
-
|
840
|
-
def _melt_data(
|
841
|
-
df: pd.DataFrame,
|
842
|
-
columns_to_leave_alone: list[str],
|
843
|
-
columns_to_melt: list[str] | None,
|
844
|
-
new_y_column_name: str,
|
845
|
-
new_color_column_name: str,
|
846
|
-
) -> pd.DataFrame:
|
847
|
-
"""Converts a wide-format dataframe to a long-format dataframe."""
|
848
|
-
import pandas as pd
|
849
|
-
from pandas.api.types import infer_dtype
|
850
|
-
|
851
|
-
melted_df = pd.melt(
|
852
|
-
df,
|
853
|
-
id_vars=columns_to_leave_alone,
|
854
|
-
value_vars=columns_to_melt,
|
855
|
-
var_name=new_color_column_name,
|
856
|
-
value_name=new_y_column_name,
|
857
|
-
)
|
858
|
-
|
859
|
-
y_series = melted_df[new_y_column_name]
|
860
|
-
if (
|
861
|
-
y_series.dtype == "object"
|
862
|
-
and "mixed" in infer_dtype(y_series)
|
863
|
-
and len(y_series.unique()) > 100
|
864
|
-
):
|
865
|
-
raise StreamlitAPIException(
|
866
|
-
"The columns used for rendering the chart contain too many values with mixed types. Please select the columns manually via the y parameter."
|
867
|
-
)
|
868
|
-
|
869
|
-
# Arrow has problems with object types after melting two different dtypes
|
870
|
-
# pyarrow.lib.ArrowTypeError: "Expected a <TYPE> object, got a object"
|
871
|
-
fixed_df = type_util.fix_arrow_incompatible_column_types(
|
872
|
-
melted_df,
|
873
|
-
selected_columns=[
|
874
|
-
*columns_to_leave_alone,
|
875
|
-
new_color_column_name,
|
876
|
-
new_y_column_name,
|
877
|
-
],
|
878
|
-
)
|
879
|
-
|
880
|
-
return fixed_df
|
881
|
-
|
882
|
-
|
883
|
-
def prep_data(
|
884
|
-
df: pd.DataFrame,
|
885
|
-
x_column: str | None,
|
886
|
-
y_column_list: list[str],
|
887
|
-
color_column: str | None,
|
888
|
-
size_column: str | None,
|
889
|
-
) -> tuple[pd.DataFrame, str | None, str | None, str | None, str | None]:
|
890
|
-
"""Prepares the data for charting. This is also used in add_rows.
|
891
|
-
|
892
|
-
Returns the prepared dataframe and the new names of the x column (taking the index reset into
|
893
|
-
consideration) and y, color, and size columns.
|
894
|
-
"""
|
895
|
-
|
896
|
-
# If y is provided, but x is not, we'll use the index as x.
|
897
|
-
# So we need to pull the index into its own column.
|
898
|
-
x_column = _maybe_reset_index_in_place(df, x_column, y_column_list)
|
899
|
-
|
900
|
-
# Drop columns we're not using.
|
901
|
-
selected_data = _drop_unused_columns(
|
902
|
-
df, x_column, color_column, size_column, *y_column_list
|
903
|
-
)
|
904
|
-
|
905
|
-
# Maybe convert color to Vega colors.
|
906
|
-
_maybe_convert_color_column_in_place(selected_data, color_column)
|
907
|
-
|
908
|
-
# Make sure all columns have string names.
|
909
|
-
(
|
910
|
-
x_column,
|
911
|
-
y_column_list,
|
912
|
-
color_column,
|
913
|
-
size_column,
|
914
|
-
) = _convert_col_names_to_str_in_place(
|
915
|
-
selected_data, x_column, y_column_list, color_column, size_column
|
916
|
-
)
|
917
|
-
|
918
|
-
# Maybe melt data from wide format into long format.
|
919
|
-
melted_data, y_column, color_column = _maybe_melt(
|
920
|
-
selected_data, x_column, y_column_list, color_column, size_column
|
921
|
-
)
|
922
|
-
|
923
|
-
# Return the data, but also the new names to use for x, y, and color.
|
924
|
-
return melted_data, x_column, y_column, color_column, size_column
|
925
|
-
|
926
|
-
|
927
|
-
def _generate_chart(
|
928
|
-
chart_type: ChartType,
|
929
|
-
data: Data | None,
|
930
|
-
x_from_user: str | None = None,
|
931
|
-
y_from_user: str | Sequence[str] | None = None,
|
932
|
-
color_from_user: str | Color | list[Color] | None = None,
|
933
|
-
size_from_user: str | float | None = None,
|
934
|
-
width: int = 0,
|
935
|
-
height: int = 0,
|
936
|
-
) -> tuple[alt.Chart, AddRowsMetadata]:
|
937
|
-
"""Function to use the chart's type, data columns and indices to figure out the chart's spec."""
|
938
|
-
import altair as alt
|
939
|
-
|
940
|
-
df = type_util.convert_anything_to_df(data, ensure_copy=True)
|
941
|
-
|
942
|
-
# From now on, use "df" instead of "data". Deleting "data" to guarantee we follow this.
|
943
|
-
del data
|
944
|
-
|
945
|
-
# Convert arguments received from the user to things Vega-Lite understands.
|
946
|
-
# Get name of column to use for x.
|
947
|
-
x_column = _parse_x_column(df, x_from_user)
|
948
|
-
# Get name of columns to use for y.
|
949
|
-
y_column_list = _parse_y_columns(df, y_from_user, x_column)
|
950
|
-
# Get name of column to use for color, or constant value to use. Any/both could be None.
|
951
|
-
color_column, color_value = _parse_generic_column(df, color_from_user)
|
952
|
-
# Get name of column to use for size, or constant value to use. Any/both could be None.
|
953
|
-
size_column, size_value = _parse_generic_column(df, size_from_user)
|
954
|
-
|
955
|
-
# Store some info so we can use it in add_rows.
|
956
|
-
add_rows_metadata = AddRowsMetadata(
|
957
|
-
# The last index of df so we can adjust the input df in add_rows:
|
958
|
-
last_index=last_index_for_melted_dataframes(df),
|
959
|
-
# This is the input to prep_data (except for the df):
|
960
|
-
columns=dict(
|
961
|
-
x_column=x_column,
|
962
|
-
y_column_list=y_column_list,
|
963
|
-
color_column=color_column,
|
964
|
-
size_column=size_column,
|
965
|
-
),
|
966
|
-
)
|
967
|
-
|
968
|
-
# At this point, all foo_column variables are either None/empty or contain actual
|
969
|
-
# columns that are guaranteed to exist.
|
970
|
-
|
971
|
-
df, x_column, y_column, color_column, size_column = prep_data(
|
972
|
-
df, x_column, y_column_list, color_column, size_column
|
973
|
-
)
|
974
|
-
|
975
|
-
# At this point, x_column is only None if user did not provide one AND df is empty.
|
976
|
-
|
977
|
-
# Create a Chart with x and y encodings.
|
978
|
-
chart = alt.Chart(
|
979
|
-
data=df,
|
980
|
-
mark=chart_type.value["mark_type"],
|
981
|
-
width=width,
|
982
|
-
height=height,
|
983
|
-
).encode(
|
984
|
-
x=_get_x_encoding(df, x_column, x_from_user, chart_type),
|
985
|
-
y=_get_y_encoding(df, y_column, y_from_user),
|
986
|
-
)
|
987
|
-
|
988
|
-
# Set up opacity encoding.
|
989
|
-
opacity_enc = _get_opacity_encoding(chart_type, color_column)
|
990
|
-
if opacity_enc is not None:
|
991
|
-
chart = chart.encode(opacity=opacity_enc)
|
992
|
-
|
993
|
-
# Set up color encoding.
|
994
|
-
color_enc = _get_color_encoding(
|
995
|
-
df, color_value, color_column, y_column_list, color_from_user
|
996
|
-
)
|
997
|
-
if color_enc is not None:
|
998
|
-
chart = chart.encode(color=color_enc)
|
999
|
-
|
1000
|
-
# Set up size encoding.
|
1001
|
-
size_enc = _get_size_encoding(chart_type, size_column, size_value)
|
1002
|
-
if size_enc is not None:
|
1003
|
-
chart = chart.encode(size=size_enc)
|
1004
|
-
|
1005
|
-
# Set up tooltip encoding.
|
1006
|
-
if x_column is not None and y_column is not None:
|
1007
|
-
chart = chart.encode(
|
1008
|
-
tooltip=_get_tooltip_encoding(
|
1009
|
-
x_column,
|
1010
|
-
y_column,
|
1011
|
-
size_column,
|
1012
|
-
color_column,
|
1013
|
-
color_enc,
|
1014
|
-
)
|
1015
|
-
)
|
1016
|
-
|
1017
|
-
return chart.interactive(), add_rows_metadata
|
1018
|
-
|
1019
|
-
|
1020
|
-
def _maybe_reset_index_in_place(
|
1021
|
-
df: pd.DataFrame, x_column: str | None, y_column_list: list[str]
|
1022
|
-
) -> str | None:
|
1023
|
-
if x_column is None and len(y_column_list) > 0:
|
1024
|
-
if df.index.name is None:
|
1025
|
-
# Pick column name that is unlikely to collide with user-given names.
|
1026
|
-
x_column = SEPARATED_INDEX_COLUMN_NAME
|
1027
|
-
else:
|
1028
|
-
# Reuse index's name for the new column.
|
1029
|
-
x_column = df.index.name
|
1030
|
-
|
1031
|
-
df.index.name = x_column
|
1032
|
-
df.reset_index(inplace=True)
|
1033
|
-
|
1034
|
-
return x_column
|
1035
|
-
|
1036
|
-
|
1037
|
-
def _drop_unused_columns(df: pd.DataFrame, *column_names: str | None) -> pd.DataFrame:
|
1038
|
-
"""Returns a subset of df, selecting only column_names that aren't None."""
|
1039
|
-
|
1040
|
-
# We can't just call set(col_names) because sets don't have stable ordering,
|
1041
|
-
# which means tests that depend on ordering will fail.
|
1042
|
-
# Performance-wise, it's not a problem, though, since this function is only ever
|
1043
|
-
# used on very small lists.
|
1044
|
-
seen = set()
|
1045
|
-
keep = []
|
1046
|
-
|
1047
|
-
for x in column_names:
|
1048
|
-
if x is None:
|
1049
|
-
continue
|
1050
|
-
if x in seen:
|
1051
|
-
continue
|
1052
|
-
seen.add(x)
|
1053
|
-
keep.append(x)
|
1054
|
-
|
1055
|
-
return df[keep]
|
1056
|
-
|
1057
|
-
|
1058
|
-
def _maybe_convert_color_column_in_place(df: pd.DataFrame, color_column: str | None):
|
1059
|
-
"""If needed, convert color column to a format Vega understands."""
|
1060
|
-
if color_column is None or len(df[color_column]) == 0:
|
1061
|
-
return
|
1062
|
-
|
1063
|
-
first_color_datum = df[color_column].iat[0]
|
1064
|
-
|
1065
|
-
if is_hex_color_like(first_color_datum):
|
1066
|
-
# Hex is already CSS-valid.
|
1067
|
-
pass
|
1068
|
-
elif is_color_tuple_like(first_color_datum):
|
1069
|
-
# Tuples need to be converted to CSS-valid.
|
1070
|
-
df[color_column] = df[color_column].map(to_css_color)
|
1071
|
-
else:
|
1072
|
-
# Other kinds of colors columns (i.e. pure numbers or nominal strings) shouldn't
|
1073
|
-
# be converted since they are treated by Vega-Lite as sequential or categorical colors.
|
1074
|
-
pass
|
1075
|
-
|
1076
|
-
|
1077
|
-
def _convert_col_names_to_str_in_place(
|
1078
|
-
df: pd.DataFrame,
|
1079
|
-
x_column: str | None,
|
1080
|
-
y_column_list: list[str],
|
1081
|
-
color_column: str | None,
|
1082
|
-
size_column: str | None,
|
1083
|
-
) -> tuple[str | None, list[str], str | None, str | None]:
|
1084
|
-
"""Converts column names to strings, since Vega-Lite does not accept ints, etc."""
|
1085
|
-
import pandas as pd
|
1086
|
-
|
1087
|
-
column_names = list(df.columns) # list() converts RangeIndex, etc, to regular list.
|
1088
|
-
str_column_names = [str(c) for c in column_names]
|
1089
|
-
df.columns = pd.Index(str_column_names)
|
1090
|
-
|
1091
|
-
return (
|
1092
|
-
None if x_column is None else str(x_column),
|
1093
|
-
[str(c) for c in y_column_list],
|
1094
|
-
None if color_column is None else str(color_column),
|
1095
|
-
None if size_column is None else str(size_column),
|
1096
|
-
)
|
1097
|
-
|
1098
|
-
|
1099
|
-
def _parse_generic_column(
|
1100
|
-
df: pd.DataFrame, column_or_value: Any
|
1101
|
-
) -> tuple[str | None, Any]:
|
1102
|
-
if isinstance(column_or_value, str) and column_or_value in df.columns:
|
1103
|
-
column_name = column_or_value
|
1104
|
-
value = None
|
1105
|
-
else:
|
1106
|
-
column_name = None
|
1107
|
-
value = column_or_value
|
1108
|
-
|
1109
|
-
return column_name, value
|
1110
|
-
|
1111
|
-
|
1112
|
-
def _parse_x_column(df: pd.DataFrame, x_from_user: str | None) -> str | None:
|
1113
|
-
if x_from_user is None:
|
1114
|
-
return None
|
1115
|
-
|
1116
|
-
elif isinstance(x_from_user, str):
|
1117
|
-
if x_from_user not in df.columns:
|
1118
|
-
raise StreamlitColumnNotFoundError(df, x_from_user)
|
1119
|
-
|
1120
|
-
return x_from_user
|
1121
|
-
|
1122
|
-
else:
|
1123
|
-
raise StreamlitAPIException(
|
1124
|
-
"x parameter should be a column name (str) or None to use the "
|
1125
|
-
f" dataframe's index. Value given: {x_from_user} "
|
1126
|
-
f"(type {type(x_from_user)})"
|
904
|
+
return self._altair_chart(
|
905
|
+
altair_chart, use_container_width=use_container_width, theme=theme
|
1127
906
|
)
|
1128
907
|
|
908
|
+
@gather_metrics("vega_lite_chart")
|
909
|
+
def vega_lite_chart(
|
910
|
+
self,
|
911
|
+
data: Data = None,
|
912
|
+
spec: dict[str, Any] | None = None,
|
913
|
+
use_container_width: bool = False,
|
914
|
+
theme: Literal["streamlit"] | None = "streamlit",
|
915
|
+
**kwargs: Any,
|
916
|
+
) -> DeltaGenerator:
|
917
|
+
"""Display a chart using the Vega-Lite library.
|
1129
918
|
|
1130
|
-
|
1131
|
-
|
1132
|
-
|
1133
|
-
|
1134
|
-
|
1135
|
-
y_column_list: list[str] = []
|
1136
|
-
|
1137
|
-
if y_from_user is None:
|
1138
|
-
y_column_list = list(df.columns)
|
1139
|
-
|
1140
|
-
elif isinstance(y_from_user, str):
|
1141
|
-
y_column_list = [y_from_user]
|
1142
|
-
|
1143
|
-
elif type_util.is_sequence(y_from_user):
|
1144
|
-
y_column_list = list(str(col) for col in y_from_user)
|
1145
|
-
|
1146
|
-
else:
|
1147
|
-
raise StreamlitAPIException(
|
1148
|
-
"y parameter should be a column name (str) or list thereof. "
|
1149
|
-
f"Value given: {y_from_user} (type {type(y_from_user)})"
|
1150
|
-
)
|
1151
|
-
|
1152
|
-
for col in y_column_list:
|
1153
|
-
if col not in df.columns:
|
1154
|
-
raise StreamlitColumnNotFoundError(df, col)
|
919
|
+
Parameters
|
920
|
+
----------
|
921
|
+
data : pandas.DataFrame, pandas.Styler, pyarrow.Table, numpy.ndarray, Iterable, dict, or None
|
922
|
+
Either the data to be plotted or a Vega-Lite spec containing the
|
923
|
+
data (which more closely follows the Vega-Lite API).
|
1155
924
|
|
1156
|
-
|
1157
|
-
|
1158
|
-
|
925
|
+
spec : dict or None
|
926
|
+
The Vega-Lite spec for the chart. If the spec was already passed in
|
927
|
+
the previous argument, this must be set to None. See
|
928
|
+
https://vega.github.io/vega-lite/docs/ for more info.
|
1159
929
|
|
1160
|
-
|
930
|
+
use_container_width : bool
|
931
|
+
If True, set the chart width to the column width. This takes
|
932
|
+
precedence over Vega-Lite's native `width` value.
|
1161
933
|
|
934
|
+
theme : "streamlit" or None
|
935
|
+
The theme of the chart. Currently, we only support "streamlit" for the Streamlit
|
936
|
+
defined design or None to fallback to the default behavior of the library.
|
1162
937
|
|
1163
|
-
|
1164
|
-
|
1165
|
-
) -> alt.OpacityValue | None:
|
1166
|
-
import altair as alt
|
938
|
+
**kwargs : any
|
939
|
+
Same as spec, but as keywords.
|
1167
940
|
|
1168
|
-
|
1169
|
-
|
941
|
+
Example
|
942
|
+
-------
|
943
|
+
>>> import streamlit as st
|
944
|
+
>>> import pandas as pd
|
945
|
+
>>> import numpy as np
|
946
|
+
>>>
|
947
|
+
>>> chart_data = pd.DataFrame(np.random.randn(200, 3), columns=["a", "b", "c"])
|
948
|
+
>>>
|
949
|
+
>>> st.vega_lite_chart(
|
950
|
+
... chart_data,
|
951
|
+
... {
|
952
|
+
... "mark": {"type": "circle", "tooltip": True},
|
953
|
+
... "encoding": {
|
954
|
+
... "x": {"field": "a", "type": "quantitative"},
|
955
|
+
... "y": {"field": "b", "type": "quantitative"},
|
956
|
+
... "size": {"field": "c", "type": "quantitative"},
|
957
|
+
... "color": {"field": "c", "type": "quantitative"},
|
958
|
+
... },
|
959
|
+
... },
|
960
|
+
... )
|
1170
961
|
|
1171
|
-
|
962
|
+
.. output::
|
963
|
+
https://doc-vega-lite-chart.streamlit.app/
|
964
|
+
height: 300px
|
1172
965
|
|
966
|
+
Examples of Vega-Lite usage without Streamlit can be found at
|
967
|
+
https://vega.github.io/vega-lite/examples/. Most of those can be easily
|
968
|
+
translated to the syntax shown above.
|
1173
969
|
|
1174
|
-
|
1175
|
-
|
1176
|
-
|
1177
|
-
|
1178
|
-
|
1179
|
-
|
1180
|
-
|
1181
|
-
return alt.Axis(tickMinStep=1, grid=grid)
|
1182
|
-
|
1183
|
-
return alt.Axis(grid=grid)
|
1184
|
-
|
1185
|
-
|
1186
|
-
def _maybe_melt(
|
1187
|
-
df: pd.DataFrame,
|
1188
|
-
x_column: str | None,
|
1189
|
-
y_column_list: list[str],
|
1190
|
-
color_column: str | None,
|
1191
|
-
size_column: str | None,
|
1192
|
-
) -> tuple[pd.DataFrame, str | None, str | None]:
|
1193
|
-
"""If multiple columns are set for y, melt the dataframe into long format."""
|
1194
|
-
y_column: str | None
|
1195
|
-
|
1196
|
-
if len(y_column_list) == 0:
|
1197
|
-
y_column = None
|
1198
|
-
elif len(y_column_list) == 1:
|
1199
|
-
y_column = y_column_list[0]
|
1200
|
-
elif x_column is not None:
|
1201
|
-
# Pick column names that are unlikely to collide with user-given names.
|
1202
|
-
y_column = MELTED_Y_COLUMN_NAME
|
1203
|
-
color_column = MELTED_COLOR_COLUMN_NAME
|
1204
|
-
|
1205
|
-
columns_to_leave_alone = [x_column]
|
1206
|
-
if size_column:
|
1207
|
-
columns_to_leave_alone.append(size_column)
|
1208
|
-
|
1209
|
-
df = _melt_data(
|
1210
|
-
df=df,
|
1211
|
-
columns_to_leave_alone=columns_to_leave_alone,
|
1212
|
-
columns_to_melt=y_column_list,
|
1213
|
-
new_y_column_name=y_column,
|
1214
|
-
new_color_column_name=color_column,
|
970
|
+
"""
|
971
|
+
return self._vega_lite_chart(
|
972
|
+
data=data,
|
973
|
+
spec=spec,
|
974
|
+
use_container_width=use_container_width,
|
975
|
+
theme=theme,
|
976
|
+
**kwargs,
|
1215
977
|
)
|
1216
978
|
|
1217
|
-
|
1218
|
-
|
1219
|
-
|
1220
|
-
|
1221
|
-
|
1222
|
-
|
1223
|
-
|
1224
|
-
|
1225
|
-
|
1226
|
-
|
1227
|
-
|
1228
|
-
|
1229
|
-
|
1230
|
-
|
1231
|
-
|
1232
|
-
x_title = ""
|
1233
|
-
elif x_column == SEPARATED_INDEX_COLUMN_NAME:
|
1234
|
-
# If the x column name is the crazy anti-collision name we gave it, then need to set
|
1235
|
-
# up a title so we never show the crazy name to the user.
|
1236
|
-
x_field = x_column
|
1237
|
-
# Don't show a label in the x axis (not even a nice label like
|
1238
|
-
# SEPARATED_INDEX_COLUMN_TITLE) when we pull the x axis from the index.
|
1239
|
-
x_title = ""
|
1240
|
-
else:
|
1241
|
-
x_field = x_column
|
1242
|
-
|
1243
|
-
# Only show a label in the x axis if the user passed a column explicitly. We
|
1244
|
-
# could go either way here, but I'm keeping this to avoid breaking the existing
|
1245
|
-
# behavior.
|
1246
|
-
if x_from_user is None:
|
1247
|
-
x_title = ""
|
1248
|
-
else:
|
1249
|
-
x_title = x_column
|
1250
|
-
|
1251
|
-
return alt.X(
|
1252
|
-
x_field,
|
1253
|
-
title=x_title,
|
1254
|
-
type=_get_x_encoding_type(df, chart_type, x_column),
|
1255
|
-
scale=alt.Scale(),
|
1256
|
-
axis=_get_axis_config(df, x_column, grid=False),
|
1257
|
-
)
|
1258
|
-
|
1259
|
-
|
1260
|
-
def _get_y_encoding(
|
1261
|
-
df: pd.DataFrame,
|
1262
|
-
y_column: str | None,
|
1263
|
-
y_from_user: str | Sequence[str] | None,
|
1264
|
-
) -> alt.Y:
|
1265
|
-
import altair as alt
|
1266
|
-
|
1267
|
-
if y_column is None:
|
1268
|
-
# If no field is specified, the full axis disappears when no data is present.
|
1269
|
-
# Maybe a bug in vega-lite? So we pass a field that doesn't exist.
|
1270
|
-
y_field = NON_EXISTENT_COLUMN_NAME
|
1271
|
-
y_title = ""
|
1272
|
-
elif y_column == MELTED_Y_COLUMN_NAME:
|
1273
|
-
# If the y column name is the crazy anti-collision name we gave it, then need to set
|
1274
|
-
# up a title so we never show the crazy name to the user.
|
1275
|
-
y_field = y_column
|
1276
|
-
# Don't show a label in the y axis (not even a nice label like
|
1277
|
-
# MELTED_Y_COLUMN_TITLE) when we pull the x axis from the index.
|
1278
|
-
y_title = ""
|
1279
|
-
else:
|
1280
|
-
y_field = y_column
|
1281
|
-
|
1282
|
-
# Only show a label in the y axis if the user passed a column explicitly. We
|
1283
|
-
# could go either way here, but I'm keeping this to avoid breaking the existing
|
1284
|
-
# behavior.
|
1285
|
-
if y_from_user is None:
|
1286
|
-
y_title = ""
|
1287
|
-
else:
|
1288
|
-
y_title = y_column
|
1289
|
-
|
1290
|
-
return alt.Y(
|
1291
|
-
field=y_field,
|
1292
|
-
title=y_title,
|
1293
|
-
type=_get_y_encoding_type(df, y_column),
|
1294
|
-
scale=alt.Scale(),
|
1295
|
-
axis=_get_axis_config(df, y_column, grid=True),
|
1296
|
-
)
|
1297
|
-
|
1298
|
-
|
1299
|
-
def _get_color_encoding(
|
1300
|
-
df: pd.DataFrame,
|
1301
|
-
color_value: Color | None,
|
1302
|
-
color_column: str | None,
|
1303
|
-
y_column_list: list[str],
|
1304
|
-
color_from_user: str | Color | list[Color] | None,
|
1305
|
-
) -> alt.Color | alt.ColorValue | None:
|
1306
|
-
import altair as alt
|
1307
|
-
|
1308
|
-
has_color_value = color_value not in [None, [], tuple()]
|
1309
|
-
|
1310
|
-
# If user passed a color value, that should win over colors coming from the
|
1311
|
-
# color column (be they manual or auto-assigned due to melting)
|
1312
|
-
if has_color_value:
|
1313
|
-
# If the color value is color-like, return that.
|
1314
|
-
if is_color_like(cast(Any, color_value)):
|
1315
|
-
if len(y_column_list) != 1:
|
1316
|
-
raise StreamlitColorLengthError([color_value], y_column_list)
|
1317
|
-
|
1318
|
-
return alt.ColorValue(to_css_color(cast(Any, color_value)))
|
1319
|
-
|
1320
|
-
# If the color value is a list of colors of approriate length, return that.
|
1321
|
-
elif isinstance(color_value, (list, tuple)):
|
1322
|
-
color_values = cast(Collection[Color], color_value)
|
1323
|
-
|
1324
|
-
if len(color_values) != len(y_column_list):
|
1325
|
-
raise StreamlitColorLengthError(color_values, y_column_list)
|
1326
|
-
|
1327
|
-
if len(color_value) == 1:
|
1328
|
-
return alt.ColorValue(to_css_color(cast(Any, color_value[0])))
|
1329
|
-
else:
|
1330
|
-
return alt.Color(
|
1331
|
-
field=color_column,
|
1332
|
-
scale=alt.Scale(range=[to_css_color(c) for c in color_values]),
|
1333
|
-
legend=COLOR_LEGEND_SETTINGS,
|
1334
|
-
type="nominal",
|
1335
|
-
title=" ",
|
1336
|
-
)
|
1337
|
-
|
1338
|
-
raise StreamlitInvalidColorError(df, color_from_user)
|
1339
|
-
|
1340
|
-
elif color_column is not None:
|
1341
|
-
column_type: str | tuple[str, list[Any]]
|
1342
|
-
|
1343
|
-
if color_column == MELTED_COLOR_COLUMN_NAME:
|
1344
|
-
column_type = "nominal"
|
1345
|
-
else:
|
1346
|
-
column_type = type_util.infer_vegalite_type(df[color_column])
|
1347
|
-
|
1348
|
-
color_enc = alt.Color(
|
1349
|
-
field=color_column, legend=COLOR_LEGEND_SETTINGS, type=column_type
|
979
|
+
def _altair_chart(
|
980
|
+
self,
|
981
|
+
altair_chart: alt.Chart,
|
982
|
+
use_container_width: bool = False,
|
983
|
+
theme: Literal["streamlit"] | None = "streamlit",
|
984
|
+
add_rows_metadata: AddRowsMetadata | None = None,
|
985
|
+
) -> DeltaGenerator:
|
986
|
+
"""Internal method to enqueue a vega-lite chart element based on an Altair chart."""
|
987
|
+
vega_lite_spec = _convert_altair_to_vega_lite_spec(altair_chart)
|
988
|
+
return self._vega_lite_chart(
|
989
|
+
data=None, # The data is already part of the spec
|
990
|
+
spec=vega_lite_spec,
|
991
|
+
use_container_width=use_container_width,
|
992
|
+
theme=theme,
|
993
|
+
add_rows_metadata=add_rows_metadata,
|
1350
994
|
)
|
1351
995
|
|
1352
|
-
|
1353
|
-
|
1354
|
-
|
1355
|
-
|
1356
|
-
|
1357
|
-
|
1358
|
-
|
1359
|
-
|
1360
|
-
|
1361
|
-
|
1362
|
-
color_enc["scale"] = alt.Scale(range=color_range)
|
1363
|
-
# Don't show the color legend, because it will just show text with the color values,
|
1364
|
-
# like #f00, #00f, etc, which are not user-readable.
|
1365
|
-
color_enc["legend"] = None
|
1366
|
-
|
1367
|
-
# Otherwise, let Vega-Lite auto-assign colors.
|
1368
|
-
# This codepath is typically reached when the color column contains numbers (in which case
|
1369
|
-
# Vega-Lite uses a color gradient to represent them) or strings (in which case Vega-Lite
|
1370
|
-
# assigns one color for each unique value).
|
1371
|
-
else:
|
1372
|
-
pass
|
1373
|
-
|
1374
|
-
return color_enc
|
1375
|
-
|
1376
|
-
return None
|
1377
|
-
|
1378
|
-
|
1379
|
-
def _get_size_encoding(
|
1380
|
-
chart_type: ChartType,
|
1381
|
-
size_column: str | None,
|
1382
|
-
size_value: str | float | None,
|
1383
|
-
) -> alt.Size | alt.SizeValue | None:
|
1384
|
-
import altair as alt
|
1385
|
-
|
1386
|
-
if chart_type == ChartType.SCATTER:
|
1387
|
-
if size_column is not None:
|
1388
|
-
return alt.Size(
|
1389
|
-
size_column,
|
1390
|
-
legend=SIZE_LEGEND_SETTINGS,
|
1391
|
-
)
|
996
|
+
def _vega_lite_chart(
|
997
|
+
self,
|
998
|
+
data: Data = None,
|
999
|
+
spec: dict[str, Any] | None = None,
|
1000
|
+
use_container_width: bool = False,
|
1001
|
+
theme: Literal["streamlit"] | None = "streamlit",
|
1002
|
+
add_rows_metadata: AddRowsMetadata | None = None,
|
1003
|
+
**kwargs: Any,
|
1004
|
+
) -> DeltaGenerator:
|
1005
|
+
"""Internal method to enqueue a vega-lite chart element based on a vega-lite spec."""
|
1392
1006
|
|
1393
|
-
|
1394
|
-
return alt.SizeValue(size_value)
|
1395
|
-
elif size_value is None:
|
1396
|
-
return alt.SizeValue(100)
|
1397
|
-
else:
|
1007
|
+
if theme not in ["streamlit", None]:
|
1398
1008
|
raise StreamlitAPIException(
|
1399
|
-
f"
|
1400
|
-
)
|
1401
|
-
|
1402
|
-
elif size_column is not None or size_value is not None:
|
1403
|
-
raise Error(
|
1404
|
-
f"Chart type {chart_type.name} does not support size argument. "
|
1405
|
-
"This should never happen!"
|
1406
|
-
)
|
1407
|
-
|
1408
|
-
return None
|
1409
|
-
|
1410
|
-
|
1411
|
-
def _get_tooltip_encoding(
|
1412
|
-
x_column: str,
|
1413
|
-
y_column: str,
|
1414
|
-
size_column: str | None,
|
1415
|
-
color_column: str | None,
|
1416
|
-
color_enc: alt.Color | alt.ColorValue | None,
|
1417
|
-
) -> list[alt.Tooltip]:
|
1418
|
-
import altair as alt
|
1419
|
-
|
1420
|
-
tooltip = []
|
1421
|
-
|
1422
|
-
# If the x column name is the crazy anti-collision name we gave it, then need to set
|
1423
|
-
# up a tooltip title so we never show the crazy name to the user.
|
1424
|
-
if x_column == SEPARATED_INDEX_COLUMN_NAME:
|
1425
|
-
tooltip.append(alt.Tooltip(x_column, title=SEPARATED_INDEX_COLUMN_TITLE))
|
1426
|
-
else:
|
1427
|
-
tooltip.append(alt.Tooltip(x_column))
|
1428
|
-
|
1429
|
-
# If the y column name is the crazy anti-collision name we gave it, then need to set
|
1430
|
-
# up a tooltip title so we never show the crazy name to the user.
|
1431
|
-
if y_column == MELTED_Y_COLUMN_NAME:
|
1432
|
-
tooltip.append(
|
1433
|
-
alt.Tooltip(
|
1434
|
-
y_column,
|
1435
|
-
title=MELTED_Y_COLUMN_TITLE,
|
1436
|
-
type="quantitative", # Just picked something random. Doesn't really matter!
|
1437
|
-
)
|
1438
|
-
)
|
1439
|
-
else:
|
1440
|
-
tooltip.append(alt.Tooltip(y_column))
|
1441
|
-
|
1442
|
-
# If we earlier decided that there should be no color legend, that's because the
|
1443
|
-
# user passed a color column with actual color values (like "#ff0"), so we should
|
1444
|
-
# not show the color values in the tooltip.
|
1445
|
-
if color_column and getattr(color_enc, "legend", True) is not None:
|
1446
|
-
# Use a human-readable title for the color.
|
1447
|
-
if color_column == MELTED_COLOR_COLUMN_NAME:
|
1448
|
-
tooltip.append(
|
1449
|
-
alt.Tooltip(
|
1450
|
-
color_column,
|
1451
|
-
title=MELTED_COLOR_COLUMN_TITLE,
|
1452
|
-
type="nominal",
|
1453
|
-
)
|
1009
|
+
f'You set theme="{theme}" while Streamlit charts only support theme=”streamlit” or theme=None to fallback to the default library theme.'
|
1454
1010
|
)
|
1455
|
-
else:
|
1456
|
-
tooltip.append(alt.Tooltip(color_column))
|
1457
|
-
|
1458
|
-
if size_column:
|
1459
|
-
tooltip.append(alt.Tooltip(size_column))
|
1460
|
-
|
1461
|
-
return tooltip
|
1462
|
-
|
1463
1011
|
|
1464
|
-
|
1465
|
-
|
1466
|
-
)
|
1467
|
-
|
1468
|
-
|
1012
|
+
# Support passing data inside spec['datasets'] and spec['data'].
|
1013
|
+
# (The data gets pulled out of the spec dict later on.)
|
1014
|
+
if isinstance(data, dict) and spec is None:
|
1015
|
+
spec = data
|
1016
|
+
data = None
|
1469
1017
|
|
1470
|
-
|
1471
|
-
# https://github.com/streamlit/streamlit/pull/2097#issuecomment-714802475
|
1472
|
-
if chart_type == ChartType.BAR and not _is_date_column(df, x_column):
|
1473
|
-
return "ordinal"
|
1474
|
-
|
1475
|
-
return type_util.infer_vegalite_type(df[x_column])
|
1476
|
-
|
1477
|
-
|
1478
|
-
def _get_y_encoding_type(
|
1479
|
-
df: pd.DataFrame, y_column: str | None
|
1480
|
-
) -> type_util.VegaLiteType:
|
1481
|
-
if y_column:
|
1482
|
-
return type_util.infer_vegalite_type(df[y_column])
|
1483
|
-
|
1484
|
-
return "quantitative" # Pick anything. If undefined, Vega-Lite may hide the axis.
|
1485
|
-
|
1486
|
-
|
1487
|
-
def marshall(
|
1488
|
-
vega_lite_chart: ArrowVegaLiteChartProto,
|
1489
|
-
altair_chart: alt.Chart,
|
1490
|
-
use_container_width: bool = False,
|
1491
|
-
theme: None | Literal["streamlit"] = "streamlit",
|
1492
|
-
**kwargs: Any,
|
1493
|
-
) -> None:
|
1494
|
-
"""Marshall chart's data into proto."""
|
1495
|
-
import altair as alt
|
1496
|
-
|
1497
|
-
# Normally altair_chart.to_dict() would transform the dataframe used by the
|
1498
|
-
# chart into an array of dictionaries. To avoid that, we install a
|
1499
|
-
# transformer that replaces datasets with a reference by the object id of
|
1500
|
-
# the dataframe. We then fill in the dataset manually later on.
|
1501
|
-
|
1502
|
-
datasets = {}
|
1503
|
-
|
1504
|
-
def id_transform(data) -> dict[str, str]:
|
1505
|
-
"""Altair data transformer that returns a fake named dataset with the
|
1506
|
-
object id.
|
1507
|
-
"""
|
1508
|
-
name = str(id(data))
|
1509
|
-
datasets[name] = data
|
1510
|
-
return {"name": name}
|
1511
|
-
|
1512
|
-
alt.data_transformers.register("id", id_transform) # type: ignore[attr-defined,unused-ignore]
|
1513
|
-
|
1514
|
-
# The default altair theme has some width/height defaults defined
|
1515
|
-
# which are not useful for Streamlit. Therefore, we change the theme to
|
1516
|
-
# "none" to avoid those defaults.
|
1517
|
-
with alt.themes.enable("none") if alt.themes.active == "default" else nullcontext(): # type: ignore[attr-defined,unused-ignore]
|
1518
|
-
with alt.data_transformers.enable("id"): # type: ignore[attr-defined,unused-ignore]
|
1519
|
-
chart_dict = altair_chart.to_dict()
|
1520
|
-
|
1521
|
-
# Put datasets back into the chart dict but note how they weren't
|
1522
|
-
# transformed.
|
1523
|
-
chart_dict["datasets"] = datasets
|
1018
|
+
proto = ArrowVegaLiteChartProto()
|
1524
1019
|
|
1525
|
-
|
1526
|
-
|
1527
|
-
chart_dict,
|
1528
|
-
use_container_width=use_container_width,
|
1529
|
-
theme=theme,
|
1530
|
-
**kwargs,
|
1531
|
-
)
|
1020
|
+
spec = _prepare_vega_lite_spec(spec, use_container_width, **kwargs)
|
1021
|
+
_marshall_chart_data(proto, spec, data)
|
1532
1022
|
|
1023
|
+
proto.spec = json.dumps(spec)
|
1024
|
+
proto.use_container_width = use_container_width
|
1025
|
+
proto.theme = theme or ""
|
1533
1026
|
|
1534
|
-
|
1535
|
-
|
1536
|
-
available_columns = ", ".join(str(c) for c in list(df.columns))
|
1537
|
-
message = (
|
1538
|
-
f'Data does not have a column named `"{col_name}"`. '
|
1539
|
-
f"Available columns are `{available_columns}`"
|
1027
|
+
return self.dg._enqueue(
|
1028
|
+
"arrow_vega_lite_chart", proto, add_rows_metadata=add_rows_metadata
|
1540
1029
|
)
|
1541
|
-
super().__init__(message, *args)
|
1542
|
-
|
1543
|
-
|
1544
|
-
class StreamlitInvalidColorError(StreamlitAPIException):
|
1545
|
-
def __init__(self, df, color_from_user, *args):
|
1546
|
-
", ".join(str(c) for c in list(df.columns))
|
1547
|
-
message = f"""
|
1548
|
-
This does not look like a valid color argument: `{color_from_user}`.
|
1549
|
-
|
1550
|
-
The color argument can be:
|
1551
1030
|
|
1552
|
-
|
1553
|
-
|
1554
|
-
|
1555
|
-
|
1556
|
-
* The name of a column.
|
1557
|
-
* Or a list of colors, matching the number of y columns to draw.
|
1558
|
-
"""
|
1559
|
-
super().__init__(message, *args)
|
1560
|
-
|
1561
|
-
|
1562
|
-
class StreamlitColorLengthError(StreamlitAPIException):
|
1563
|
-
def __init__(self, color_values, y_column_list, *args):
|
1564
|
-
message = (
|
1565
|
-
f"The list of colors `{color_values}` must have the same "
|
1566
|
-
"length as the list of columns to be colored "
|
1567
|
-
f"`{y_column_list}`."
|
1568
|
-
)
|
1569
|
-
super().__init__(message, *args)
|
1031
|
+
@property
|
1032
|
+
def dg(self) -> DeltaGenerator:
|
1033
|
+
"""Get our DeltaGenerator."""
|
1034
|
+
return cast("DeltaGenerator", self)
|