├── .devcontainer
├── Dockerfile
└── devcontainer.json
├── .github
└── dependabot.yml
├── .gitignore
├── LICENSE
├── MANIFEST.in
├── Readme.md
├── demo.gif
├── setup.py
└── streamlit_vega_lite
├── __init__.py
└── frontend
├── .env
├── .prettierignore
├── .prettierrc
├── package.json
├── public
└── index.html
├── src
├── VegaLiteComponent.tsx
├── arrow-loader.ts
├── index.tsx
└── react-app-env.d.ts
├── tsconfig.json
└── yarn.lock
/.devcontainer/Dockerfile:
--------------------------------------------------------------------------------
1 | #-------------------------------------------------------------------------------------------------------------
2 | # Copyright (c) Microsoft Corporation. All rights reserved.
3 | # Licensed under the MIT License. See https://go.microsoft.com/fwlink/?linkid=2090316 for license information.
4 | #-------------------------------------------------------------------------------------------------------------
5 |
6 | # Update the VARIANT arg in devcontainer.json to pick a Python version: 3, 3.8, 3.7, 3.6
7 | # To fully customize the contents of this image, use the following Dockerfile instead:
8 | # https://github.com/microsoft/vscode-dev-containers/tree/v0.128.0/containers/python-3/.devcontainer/base.Dockerfile
9 | ARG VARIANT="3"
10 | FROM mcr.microsoft.com/vscode/devcontainers/python:0-${VARIANT}
11 |
12 | # [Optional] If your requirements rarely change, uncomment this section to add them to the image.
13 | #
14 | # COPY requirements.txt /tmp/pip-tmp/
15 | # RUN pip3 --disable-pip-version-check --no-cache-dir install -r /tmp/pip-tmp/requirements.txt \
16 | # && rm -rf /tmp/pip-tmp
17 |
18 |
19 | RUN curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | sudo apt-key add -
20 | RUN echo "deb https://dl.yarnpkg.com/debian/ stable main" | sudo tee /etc/apt/sources.list.d/yarn.list
21 |
22 | RUN apt-get update \
23 | && export DEBIAN_FRONTEND=noninteractive \
24 | && apt-get -y install --no-install-recommends nodejs yarn
25 |
--------------------------------------------------------------------------------
/.devcontainer/devcontainer.json:
--------------------------------------------------------------------------------
1 | // For format details, see https://aka.ms/vscode-remote/devcontainer.json or this file's README at:
2 | // https://github.com/microsoft/vscode-dev-containers/tree/v0.128.0/containers/python-3
3 | {
4 | "name": "Python 3",
5 | "build": {
6 | "dockerfile": "Dockerfile",
7 | "context": "..",
8 | // Update 'VARIANT' to pick a Python version. Rebuild the container
9 | // if it already exists to update. Available variants: 3, 3.6, 3.7, 3.8
10 | "args": { "VARIANT": "3.8" }
11 | },
12 |
13 | // Set *default* container specific settings.json values on container create.
14 | "settings": {
15 | "terminal.integrated.shell.linux": "/bin/zsh",
16 | "python.pythonPath": "/usr/local/bin/python",
17 | "python.linting.enabled": true,
18 | "python.linting.pylintEnabled": true,
19 | "python.formatting.autopep8Path": "/usr/local/py-utils/bin/autopep8",
20 | "python.formatting.blackPath": "/usr/local/py-utils/bin/black",
21 | "python.formatting.yapfPath": "/usr/local/py-utils/bin/yapf",
22 | "python.linting.banditPath": "/usr/local/py-utils/bin/bandit",
23 | "python.linting.flake8Path": "/usr/local/py-utils/bin/flake8",
24 | "python.linting.mypyPath": "/usr/local/py-utils/bin/mypy",
25 | "python.linting.pycodestylePath": "/usr/local/py-utils/bin/pycodestyle",
26 | "python.linting.pydocstylePath": "/usr/local/py-utils/bin/pydocstyle",
27 | "python.linting.pylintPath": "/usr/local/py-utils/bin/pylint"
28 | },
29 |
30 | // Add the IDs of extensions you want installed when the container is created.
31 | "extensions": [
32 | "ms-python.python"
33 | ],
34 |
35 | // Use 'forwardPorts' to make a list of ports inside the container available locally.
36 | "forwardPorts": [8501, 3000, 3001],
37 |
38 | // Use 'postCreateCommand' to run commands after the container is created.
39 | // "postCreateCommand": "pip3 install --user -r requirements.txt",
40 |
41 | // Uncomment to connect as a non-root user. See https://aka.ms/vscode-remote/containers/non-root.
42 | // "remoteUser": "vscode"
43 | }
44 |
--------------------------------------------------------------------------------
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | version: 2
2 | updates:
3 | - package-ecosystem: pip
4 | directory: "/"
5 | schedule:
6 | interval: monthly
7 | time: "23:00"
8 | timezone: PST8PDT
9 | open-pull-requests-limit: 10
10 | - package-ecosystem: npm
11 | directory: "/"
12 | schedule:
13 | interval: monthly
14 | time: "23:00"
15 | timezone: PST8PDT
16 | open-pull-requests-limit: 10
17 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | streamlit_vega_lite/frontend/node_modules
2 | streamlit_vega_lite/frontend/build
3 | streamlit_vega_lite/frontend/yarn-error.log
4 | streamlit_vega_lite.egg-info/
5 | build
6 | dist
7 | streamlit_vega_lite/frontend/.eslintcache
8 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright (c) 2018 The Python Packaging Authority
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining a copy
4 | of this software and associated documentation files (the "Software"), to deal
5 | in the Software without restriction, including without limitation the rights
6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7 | copies of the Software, and to permit persons to whom the Software is
8 | furnished to do so, subject to the following conditions:
9 |
10 | The above copyright notice and this permission notice shall be included in all
11 | copies or substantial portions of the Software.
12 |
13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
19 | SOFTWARE.
20 |
--------------------------------------------------------------------------------
/MANIFEST.in:
--------------------------------------------------------------------------------
1 | recursive-include streamlit_vega_lite/frontend/build *
2 |
--------------------------------------------------------------------------------
/Readme.md:
--------------------------------------------------------------------------------
1 | # Streamlit Vega-Lite
2 |
3 | [](https://github.com/psf/black)
4 | [](https://pypi.org/project/streamlit-vega-lite)
5 |
6 | **🐉 Here be dragons. This is a proof of concept.**
7 |
8 | Making Vega-Lite selection created by user interactions available in Python. Works with [Altair](https://altair-viz.github.io/).
9 |
10 | For examples, see https://github.com/domoritz/streamlit-vega-lite/blob/master/streamlit_vega_lite/__init__.py. You can also try the demo at https://github.com/domoritz/streamlit-vega-lite-demo.
11 |
12 |
13 |
14 | ## Documentation
15 |
16 | ### Installation
17 |
18 | `pip install streamlit-vega-lite`
19 |
20 | ### Usage
21 |
22 | There are two functions available. `vega_lite_component` expects a Vega-Lite specification as a dictionary and any named datasets as keyword arguments. The datasets will be transferred as efficient Arrow tables. `altair_component` supports Altair charts and automatically extracts all datasets and transfers them as Arrow dataframes.
23 |
24 | #### Example
25 |
26 | ```python
27 | import altair as alt
28 | import streamlit as st
29 | import pandas as pd
30 | import numpy as np
31 |
32 | from streamlit_vega_lite import vega_lite_component, altair_component
33 |
34 | hist_data = pd.DataFrame(np.random.normal(42, 10, (200, 1)), columns=["x"])
35 |
36 | @st.cache
37 | def altair_histogram():
38 | brushed = alt.selection_interval(encodings=["x"], name="brushed")
39 |
40 | return (
41 | alt.Chart(hist_data)
42 | .mark_bar()
43 | .encode(alt.X("x:Q", bin=True), y="count()")
44 | .add_selection(brushed)
45 | )
46 |
47 | event_dict = altair_component(altair_chart=altair_histogram())
48 |
49 | r = event_dict.get("x")
50 | if r:
51 | filtered = hist_data[(hist_data.x >= r[0]) & (hist_data.x < r[1])]
52 | st.write(filtered)
53 | ```
54 |
55 | ## Dev Setup
56 |
57 | Open two terminals in the dev container using VSCode's [Remote Containers Extension](https://code.visualstudio.com/docs/remote/containers).
58 |
59 | In the first terminal, run:
60 |
61 | ```bash
62 | # Install python module in editable mode
63 | pip install -e .
64 |
65 | # Launch streamlit app
66 | streamlit run streamlit_vega_lite/__init__.py
67 | ```
68 |
69 | In the second terminal:
70 |
71 | ```bash
72 | # Switch to location of frontend code
73 | cd streamlit_vega_lite/frontend
74 | # Install dependencies
75 | yarn
76 | # Launch frontend assets
77 | yarn start
78 | ```
79 |
80 | Then open http://localhost:8501/.
81 |
82 | ## Style
83 |
84 | Run Black for Python formatting.
85 |
86 | ```
87 | black . -l 120
88 | ```
89 |
90 | Run Prettier for other formatting in the frontend directory.
91 |
92 | ```
93 | yarn format
94 | ```
95 |
96 | ## Publish
97 |
98 | See https://docs.streamlit.io/en/stable/publish_streamlit_components.html.
99 |
100 | Make sure that `_RELEASE` is set to `True`.
101 |
102 | ```sh
103 | pushd streamlit_vega_lite/frontend
104 | yarn build
105 | popd
106 | python setup.py sdist bdist_wheel
107 | python3 -m twine upload --repository pypi dist/*
108 | ```
109 |
--------------------------------------------------------------------------------
/demo.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/domoritz/streamlit-vega-lite/a72f8ca3a766d5e3e5b62ed9d57d18e37c456496/demo.gif
--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
1 | import setuptools
2 |
3 | setuptools.setup(
4 | name="streamlit-vega-lite",
5 | version="0.1.0",
6 | author="Dominik Moritz, Cameron Yick",
7 | author_email="",
8 | description="A Vega-Lite Component for Streamlit that Supports Selections",
9 | long_description="",
10 | long_description_content_type="text/plain",
11 | url="https://github.com/domoritz/streamlit-vega-lite",
12 | packages=setuptools.find_packages(),
13 | include_package_data=True,
14 | classifiers=[],
15 | python_requires=">=3.6",
16 | install_requires=[
17 | "streamlit >= 0.63",
18 | ],
19 | )
20 |
--------------------------------------------------------------------------------
/streamlit_vega_lite/__init__.py:
--------------------------------------------------------------------------------
1 | import os
2 | import streamlit.components.v1 as components
3 | import pandas as pd
4 |
5 |
6 | # Set this to False while we're developing the component, and True when we're
7 | # ready to package and distribute it.
8 | _RELEASE = False
9 |
10 | COMPONENT_NAME = "vega_lite_component"
11 |
12 | if not _RELEASE:
13 | _component_func = components.declare_component(
14 | COMPONENT_NAME,
15 | url="http://localhost:3001",
16 | )
17 | else:
18 | parent_dir = os.path.dirname(os.path.abspath(__file__))
19 | build_dir = os.path.join(parent_dir, "frontend/build")
20 | _component_func = components.declare_component(COMPONENT_NAME, path=build_dir)
21 |
22 |
23 | def vega_lite_component(spec={}, key=None, **kwargs):
24 | """Returns selections from the Vega-Lite chart.
25 |
26 | Parameters
27 | ----------
28 | spec: dict
29 | The Vega-Lite spec for the chart. See https://vega.github.io/vega-lite/docs/
30 | for more info.
31 | key: str or None
32 | An optional key that uniquely identifies this component. If this is
33 | None, and the component"s arguments are changed, the component will
34 | be re-mounted in the Streamlit frontend and lose its current state.
35 | **kwargs: pandas.DataFrame or list
36 | Datasets to be passed to the spec via named datasets.
37 |
38 | Returns
39 | -------
40 | dict
41 | The selections from the chart.
42 |
43 | """
44 |
45 | # basic argument validation
46 | if not spec.get("selection"):
47 | raise ValueError("Spec must contain selection")
48 |
49 | for selection in spec["selection"].values():
50 | if (
51 | (selection["type"] == "single" or selection["type"] == "multi")
52 | and not selection.get("encodings")
53 | and not selection.get("fields")
54 | ):
55 | raise ValueError("Every single and multi selection in spec must be projected onto encodings or fields.")
56 |
57 | return _component_func(spec=spec, **kwargs, key=key, default={})
58 |
59 |
60 | def altair_component(altair_chart, key=None):
61 | """Returns selections from the Altair chart.
62 |
63 | Parameters
64 | ----------
65 | altair_chart: altair.vegalite.v2.api.Chart
66 | The Altair chart object to display.
67 | key: str or None
68 | An optional key that uniquely identifies this component. If this is
69 | None, and the component"s arguments are changed, the component will
70 | be re-mounted in the Streamlit frontend and lose its current state.
71 |
72 | Returns
73 | -------
74 | dict
75 | The selections from the chart.
76 |
77 | """
78 |
79 | import altair as alt
80 |
81 | # Normally altair_chart.to_dict() would transform the dataframe used by the
82 | # chart into an array of dictionaries. To avoid that, we install a
83 | # transformer that replaces datasets with a reference by the object id of
84 | # the dataframe. We then fill in the dataset manually later on.
85 |
86 | datasets = {}
87 |
88 | # make a copy of the chart so that we don't have to rerender it even if nothing changed
89 | altair_chart = altair_chart.copy()
90 |
91 | def id_transform(data):
92 | """Altair data transformer that returns a fake named dataset with the
93 | object id."""
94 | name = f"d{id(data)}"
95 | datasets[name] = data
96 | return {"name": name}
97 |
98 | alt.data_transformers.register("id", id_transform)
99 |
100 | with alt.data_transformers.enable("id"):
101 | chart_dict = altair_chart.to_dict()
102 |
103 | return _component_func(spec=chart_dict, **datasets, key=key, default={})
104 |
105 |
106 | # Add some test code to play with the component while it"s in development.
107 | # During development, we can run this just as we would any other Streamlit
108 | # app: `$ streamlit run my_component/__init__.py`
109 | if not _RELEASE:
110 | import streamlit as st
111 | import numpy as np
112 | import altair as alt
113 |
114 | st.subheader("Vega-Lite + Streamlit Event Emitter")
115 |
116 | bar_spec = {
117 | "$schema": "https://vega.github.io/schema/vega-lite/v4.json",
118 | "data": {"name": "bar_data"},
119 | "selection": {"clicked": {"type": "multi", "empty": "none", "encodings": ["x"]}},
120 | "mark": "bar",
121 | "encoding": {
122 | "x": {"field": "a", "type": "nominal", "axis": {"labelAngle": 0}},
123 | "y": {"field": "b", "type": "quantitative"},
124 | "color": {
125 | "condition": {"selection": "clicked", "value": "firebrick"},
126 | "value": "steelblue",
127 | },
128 | },
129 | }
130 |
131 | bar_data = pd.DataFrame(
132 | [
133 | {"a": "A", "b": 10},
134 | {"a": "B", "b": 34},
135 | {"a": "C", "b": 55},
136 | {"a": "D", "b": 19},
137 | {"a": "E", "b": 40},
138 | {"a": "F", "b": 34},
139 | {"a": "G", "b": 91},
140 | {"a": "H", "b": 78},
141 | {"a": "I", "b": 25},
142 | ]
143 | )
144 |
145 | event_dict = vega_lite_component(spec=bar_spec, bar_data=bar_data)
146 | selection = event_dict.get("a")
147 | if selection:
148 | fields = " and ".join(selection)
149 | st.write(f"You selected on { fields }.")
150 | else:
151 | st.write("You haven't selected any bars yet.")
152 |
153 | st.subheader("Vega-Lite + Streamlit Event Emitter")
154 |
155 | hist_spec = {
156 | "$schema": "https://vega.github.io/schema/vega-lite/v4.json",
157 | "data": {"name": "hist_data"},
158 | "mark": "bar",
159 | "selection": {"brushed": {"type": "interval"}},
160 | "encoding": {"x": {"bin": True, "field": "x"}, "y": {"aggregate": "count"}},
161 | }
162 |
163 | np.random.seed(0)
164 | hist_data = pd.DataFrame(np.random.normal(42, 10, (200, 1)), columns=["x"])
165 |
166 | event_dict = vega_lite_component(spec=hist_spec, hist_data=hist_data)
167 |
168 | def print_range(r):
169 | st.write(
170 | f"You selected data in the range from {r[0]:.1f} to {r[1]:.1f}."
171 | if r
172 | else "You haven't selected anything yet."
173 | )
174 |
175 | print_range(event_dict.get("x"))
176 |
177 | st.subheader("Altair + Streamlit Event Emitter")
178 |
179 | @st.cache
180 | def make_altair_histogram():
181 | brushed = alt.selection_interval(encodings=["x"], name="brushed")
182 | return (
183 | alt.Chart(hist_data)
184 | .mark_bar()
185 | .encode(
186 | alt.X("x:Q", bin=True),
187 | y="count()",
188 | )
189 | .add_selection(brushed)
190 | )
191 |
192 | chart = make_altair_histogram()
193 | event_dict = altair_component(altair_chart=chart)
194 | r = event_dict.get("x")
195 | print_range(r)
196 |
197 | if r:
198 | filtered = hist_data[(hist_data.x >= r[0]) & (hist_data.x < r[1])]
199 | st.write(filtered.describe())
200 |
--------------------------------------------------------------------------------
/streamlit_vega_lite/frontend/.env:
--------------------------------------------------------------------------------
1 | # Run the component's dev server on :3001
2 | # (The Streamlit dev server already runs on :3000)
3 | PORT=3001
4 |
5 | # Don't automatically open the web browser on `npm run start`.
6 | BROWSER=none
7 |
--------------------------------------------------------------------------------
/streamlit_vega_lite/frontend/.prettierignore:
--------------------------------------------------------------------------------
1 | build/
2 |
--------------------------------------------------------------------------------
/streamlit_vega_lite/frontend/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "endOfLine": "lf",
3 | "semi": true,
4 | "trailingComma": "es5"
5 | }
6 |
--------------------------------------------------------------------------------
/streamlit_vega_lite/frontend/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "streamlit-vega-lite",
3 | "version": "0.1.0",
4 | "private": true,
5 | "devDependencies": {
6 | "@types/jest": "^26.0.20",
7 | "@types/node": "^14.14.20",
8 | "@types/react-dom": "^17.0.0",
9 | "@types/react": "^17.0.0",
10 | "prettier": "^2.2.1",
11 | "react-scripts": "^4.0.1",
12 | "typescript": "~4.1.3"
13 | },
14 | "dependencies": {
15 | "bootstrap": "^4.5.3",
16 | "react": "^17.0.1",
17 | "react-dom": "^17.0.1",
18 | "react-vega": "^7.4.2",
19 | "streamlit-component-lib": "^1.1.3",
20 | "vega": "^5.18.0",
21 | "vega-lite": "^4.17.0"
22 | },
23 | "scripts": {
24 | "start": "react-scripts start",
25 | "build": "react-scripts build",
26 | "test": "react-scripts test",
27 | "eject": "react-scripts eject",
28 | "format": "prettier . --write"
29 | },
30 | "eslintConfig": {
31 | "extends": "react-app"
32 | },
33 | "browserslist": {
34 | "production": [
35 | ">0.2%",
36 | "not dead",
37 | "not op_mini all"
38 | ],
39 | "development": [
40 | "last 1 chrome version",
41 | "last 1 firefox version",
42 | "last 1 safari version"
43 | ]
44 | },
45 | "homepage": "."
46 | }
47 |
--------------------------------------------------------------------------------
/streamlit_vega_lite/frontend/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |