# 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", ] )