├── .gitignore ├── LICENSE ├── MANIFEST.in ├── README.md ├── example.gif ├── setup.py └── src └── streamlit_plotly_events ├── __init__.py └── frontend ├── .env ├── .prettierrc ├── package.json ├── public └── index.html ├── src ├── StreamlitPlotlyEventsComponent.tsx ├── index.tsx └── react-app-env.d.ts └── tsconfig.json /.gitignore: -------------------------------------------------------------------------------- 1 | ######################################################################## 2 | # Python - https://github.com/github/gitignore/blob/master/Python.gitignore 3 | ######################################################################## 4 | # Byte-compiled / optimized / DLL files 5 | __pycache__/ 6 | *.py[cod] 7 | *$py.class 8 | 9 | # Distribution / packaging 10 | build/ 11 | dist/ 12 | eggs/ 13 | .eggs/ 14 | *.egg-info/ 15 | *.egg 16 | 17 | # Unit test / coverage reports 18 | .coverage 19 | .coverage\.* 20 | .pytest_cache/ 21 | .mypy_cache/ 22 | test-reports 23 | 24 | # Test fixtures 25 | cffi_bin 26 | 27 | # Pyenv Stuff 28 | .python-version 29 | 30 | ######################################################################## 31 | # OSX - https://github.com/github/gitignore/blob/master/Global/macOS.gitignore 32 | ######################################################################## 33 | .DS_Store 34 | .DocumentRevisions-V100 35 | .fseventsd 36 | .Spotlight-V100 37 | .TemporaryItems 38 | .Trashes 39 | .VolumeIcon.icns 40 | .com.apple.timemachine.donotpresent 41 | 42 | ######################################################################## 43 | # node - https://github.com/github/gitignore/blob/master/Node.gitignore 44 | ######################################################################## 45 | # Logs 46 | npm-debug.log* 47 | yarn-debug.log* 48 | yarn-error.log* 49 | 50 | # Dependency directories 51 | node_modules/ 52 | 53 | # Coverage directory used by tools like istanbul 54 | coverage/ 55 | 56 | # Lockfiles 57 | yarn.lock 58 | package-lock.json 59 | 60 | ######################################################################## 61 | # JetBrains 62 | ######################################################################## 63 | .idea 64 | 65 | ######################################################################## 66 | # VSCode 67 | ######################################################################## 68 | .vscode/ 69 | 70 | 71 | # Virtual Env 72 | env/* -------------------------------------------------------------------------------- /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 src/streamlit_plotly_events/frontend/build * 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 🔥📊📣 Streamlit Plotly Events 📣📊🔥 2 | Ever wanted to harness those awesome events from Plotly charts inside of Streamlit? So did I, so now you can! 3 | 4 | ![Example Image](example.gif) 5 | 6 | ## Overview, TL;DR 7 | ### Installation 8 | Install via Pip! 9 | 10 | ```pip install streamlit-plotly-events``` 11 | 12 | ### Usage 13 | Import the component, and use it like any other Streamlit custom component! 14 | ```python 15 | import streamlit as st 16 | from streamlit_plotly_events import plotly_events 17 | 18 | # Writes a component similar to st.write() 19 | fig = px.line(x=[1], y=[1]) 20 | selected_points = plotly_events(fig) 21 | 22 | # Can write inside of things using with! 23 | with st.beta_expander('Plot'): 24 | fig = px.line(x=[1], y=[1]) 25 | selected_points = plotly_events(fig) 26 | 27 | # Select other Plotly events by specifying kwargs 28 | fig = px.line(x=[1], y=[1]) 29 | selected_points = plotly_events(fig, click_event=False, hover_event=True) 30 | ``` 31 | 32 | What the component returns: 33 | ``` 34 | Returns 35 | ------- 36 | list of dict 37 | List of dictionaries containing point details (in case multiple overlapping points have been clicked). 38 | 39 | Details can be found here: 40 | https://plotly.com/javascript/plotlyjs-events/#event-data 41 | 42 | Format of dict: 43 | { 44 | x: int (x value of point), 45 | y: int (y value of point), 46 | curveNumber: (index of curve), 47 | pointNumber: (index of selected point), 48 | pointIndex: (index of selected point) 49 | } 50 | 51 | ``` 52 | 53 | ## Events 54 | Currently, a number of plotly events can be enabled. They can be enabled/disabled using kwargs on the `plotly_event()` function. 55 | - **Click** `click_event` (defaults to `True`): Triggers event on mouse click of point 56 | - **Select** `select_event`: Triggers event when points have been lasso 57 | - **Hover** `hover_event`: Triggers event on mouse hover of point (**WARNING: VERY RESOURCE INTENSIVE**) 58 | 59 | # Contributing 60 | Please! I'm hardly a frontend developer! I think there's a bunch of amazing functionality we can add into streamlit/plotly!! 61 | 62 | This repo follows `black` formatting standards for the Python parts of the project. 63 | 64 | Follow the instructions on the `streamlit_components` [example repository](https://github.com/streamlit/component-template) to get up and running, or follow along below! 65 | 66 | ### Quickstart 67 | 68 | * Ensure you have [Python 3.6+](https://www.python.org/downloads/), [Node.js](https://nodejs.org), and [npm](https://docs.npmjs.com/downloading-and-installing-node-js-and-npm) installed. 69 | * Clone this repo. 70 | * Create a new Python virtual environment for the template: 71 | ``` 72 | $ cd template 73 | $ python3 -m venv venv # create venv 74 | $ . venv/bin/activate # activate venv 75 | $ pip install streamlit # install streamlit 76 | $ pip install plotly # install plotly 77 | ``` 78 | * Initialize and run the component template frontend: 79 | ``` 80 | $ cd src/streamlit_plotly_events/frontend 81 | $ npm install # Install npm dependencies 82 | $ npm run start # Start the Webpack dev server 83 | ``` 84 | * From a separate terminal, run the template's Streamlit app: 85 | ``` 86 | $ cd src/streamlit_plotly_events 87 | $ . venv/bin/activate # activate the venv you created earlier 88 | $ streamlit run __init__.py # run the example server 89 | ``` 90 | -------------------------------------------------------------------------------- /example.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ethanhe42/streamlit-plotly-events/f7b517a128adff0d4f1e365e5b9670d2e7c19125/example.gif -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import setuptools 2 | 3 | setuptools.setup( 4 | name="streamlit-plotly-events", 5 | version="0.0.6", 6 | author="Ellie Jones", 7 | author_email="ellie@altaml.com", 8 | description="Plotly chart component for Streamlit that also allows for events to bubble back up to Streamlit.", 9 | long_description="Plotly chart component for Streamlit that also allows for events to bubble back up to Streamlit.", 10 | long_description_content_type="text/plain", 11 | url="https://github.com/null-jones/streamlit-plotly-events", 12 | package_dir={"": "src"}, 13 | packages=setuptools.find_packages(where="src"), 14 | include_package_data=True, 15 | classifiers=[], 16 | python_requires=">=3.6", 17 | install_requires=[ 18 | "streamlit >= 0.63", 19 | "plotly >= 4.14.3", 20 | ], 21 | ) 22 | -------------------------------------------------------------------------------- /src/streamlit_plotly_events/__init__.py: -------------------------------------------------------------------------------- 1 | import os 2 | import streamlit.components.v1 as components 3 | from json import loads 4 | 5 | # Create a _RELEASE constant. We'll set this to False while we're developing 6 | # the component, and True when we're ready to package and distribute it. 7 | # (This is, of course, optional - there are innumerable ways to manage your 8 | # release process.) 9 | _RELEASE = False 10 | 11 | # Declare a Streamlit component. `declare_component` returns a function 12 | # that is used to create instances of the component. We're naming this 13 | # function "_component_func", with an underscore prefix, because we don't want 14 | # to expose it directly to users. Instead, we will create a custom wrapper 15 | # function, below, that will serve as our component's public API. 16 | 17 | # It's worth noting that this call to `declare_component` is the 18 | # *only thing* you need to do to create the binding between Streamlit and 19 | # your component frontend. Everything else we do in this file is simply a 20 | # best practice. 21 | 22 | if not _RELEASE: 23 | _component_func = components.declare_component( 24 | # We give the component a simple, descriptive name ("my_component" 25 | # does not fit this bill, so please choose something better for your 26 | # own component :) 27 | "plotly_events", 28 | # Pass `url` here to tell Streamlit that the component will be served 29 | # by the local dev server that you run via `npm run start`. 30 | # (This is useful while your component is in development.) 31 | url="http://localhost:3001", 32 | ) 33 | else: 34 | # When we're distributing a production version of the component, we'll 35 | # replace the `url` param with `path`, and point it to to the component's 36 | # build directory: 37 | parent_dir = os.path.dirname(os.path.abspath(__file__)) 38 | build_dir = os.path.join(parent_dir, "frontend/build") 39 | _component_func = components.declare_component("plotly_events", path=build_dir) 40 | 41 | 42 | # Create a wrapper function for the component. This is an optional 43 | # best practice - we could simply expose the component function returned by 44 | # `declare_component` and call it done. The wrapper allows us to customize 45 | # our component's API: we can pre-process its input args, post-process its 46 | # output value, and add a docstring for users. 47 | def plotly_events( 48 | plot_fig, 49 | click_event=True, 50 | select_event=False, 51 | hover_event=False, 52 | override_height=450, 53 | override_width="100%", 54 | key=None, 55 | ): 56 | """Create a new instance of "plotly_events". 57 | 58 | Parameters 59 | ---------- 60 | plot_fig: Plotly Figure 61 | Plotly figure that we want to render in Streamlit 62 | click_event: boolean, default: True 63 | Watch for click events on plot and return point data when triggered 64 | select_event: boolean, default: False 65 | Watch for select events on plot and return point data when triggered 66 | hover_event: boolean, default: False 67 | Watch for hover events on plot and return point data when triggered 68 | override_height: int, default: 450 69 | Integer to override component height. Defaults to 450 (px) 70 | override_width: string, default: '100%' 71 | String (or integer) to override width. Defaults to 100% (whole width of iframe) 72 | key: str or None 73 | An optional key that uniquely identifies this component. If this is 74 | None, and the component's arguments are changed, the component will 75 | be re-mounted in the Streamlit frontend and lose its current state. 76 | 77 | Returns 78 | ------- 79 | list of dict 80 | List of dictionaries containing point details (in case multiple overlapping 81 | points have been clicked). 82 | 83 | Details can be found here: 84 | https://plotly.com/javascript/plotlyjs-events/#event-data 85 | 86 | Format of dict: 87 | { 88 | x: int (x value of point), 89 | y: int (y value of point), 90 | curveNumber: (index of curve), 91 | pointNumber: (index of selected point), 92 | pointIndex: (index of selected point) 93 | } 94 | 95 | """ 96 | # kwargs will be exposed to frontend in "args" 97 | component_value = _component_func( 98 | plot_obj=plot_fig.to_json(), 99 | override_height=override_height, 100 | override_width=override_width, 101 | key=key, 102 | click_event=click_event, 103 | select_event=select_event, 104 | hover_event=hover_event, 105 | default="[]", # Default return empty JSON list 106 | ) 107 | 108 | # Parse component_value since it's JSON and return to Streamlit 109 | return loads(component_value) 110 | 111 | 112 | # Add some test code to play with the component while it's in development. 113 | # During development, we can run this just as we would any other Streamlit 114 | # app: `$ streamlit run src/streamlit_plotly_events/__init__.py` 115 | if not _RELEASE: 116 | import streamlit as st 117 | import plotly.express as px 118 | 119 | st.set_page_config(layout="wide") 120 | 121 | st.subheader("Plotly Line Chart") 122 | fig = px.line(x=[0, 1, 2, 3], y=[0, 1, 2, 3]) 123 | plot_name_holder = st.empty() 124 | clickedPoint = plotly_events(fig, key="line") 125 | plot_name_holder.write(f"Clicked Point: {clickedPoint}") 126 | 127 | # Here we add columns to check auto-resize/etc 128 | st.subheader("Plotly Bar Chart (With columns)") 129 | _, c2, _ = st.beta_columns((1, 6, 1)) 130 | with c2: 131 | fig2 = px.bar(x=[0, 1, 2, 3], y=[0, 1, 2, 3]) 132 | plot_name_holder2 = st.empty() 133 | clickedPoint2 = plotly_events(fig2, key="bar") 134 | plot_name_holder2.write(f"Clicked Point: {clickedPoint2}") 135 | 136 | st.subheader("# Plotly Select Event") 137 | fig3 = px.bar(x=[0, 1, 2, 3], y=[0, 1, 2, 3]) 138 | plot_name_holder3 = st.empty() 139 | clickedPoint3 = plotly_events( 140 | fig3, key="select", click_event=False, select_event=True 141 | ) 142 | plot_name_holder3.write(f"Selected Point: {clickedPoint3}") 143 | 144 | st.subheader("# Plotly Hover Event") 145 | fig4 = px.bar(x=[0, 1, 2, 3], y=[0, 1, 2, 3]) 146 | plot_name_holder4 = st.empty() 147 | clickedPoint4 = plotly_events( 148 | fig4, key="hover", click_event=False, hover_event=True 149 | ) 150 | plot_name_holder4.write(f"Hovered Point: {clickedPoint4}") 151 | -------------------------------------------------------------------------------- /src/streamlit_plotly_events/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 | -------------------------------------------------------------------------------- /src/streamlit_plotly_events/frontend/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "endOfLine": "lf", 3 | "semi": false, 4 | "trailingComma": "es5" 5 | } 6 | -------------------------------------------------------------------------------- /src/streamlit_plotly_events/frontend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "streamlit_component_template", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@testing-library/jest-dom": "^4.2.4", 7 | "@testing-library/react": "^9.3.2", 8 | "@testing-library/user-event": "^7.1.2", 9 | "@types/jest": "^24.0.0", 10 | "@types/node": "^12.0.0", 11 | "@types/react": "^16.9.0", 12 | "@types/react-dom": "^16.9.0", 13 | "plotly.js": "^1.58.2", 14 | "react": "^16.13.1", 15 | "react-dom": "^16.13.1", 16 | "react-plotly.js": "^2.4.0", 17 | "react-scripts": "3.4.1", 18 | "streamlit-component-lib": "^1.2.0", 19 | "typescript": "~3.7.2" 20 | }, 21 | "devDependencies": { 22 | "@types/plotly.js": "^1.50.16", 23 | "@types/react-plotly.js": "^2.2.4" 24 | }, 25 | "scripts": { 26 | "start": "react-scripts start", 27 | "build": "react-scripts build", 28 | "test": "react-scripts test", 29 | "eject": "react-scripts eject" 30 | }, 31 | "eslintConfig": { 32 | "extends": "react-app" 33 | }, 34 | "browserslist": { 35 | "production": [ 36 | ">0.2%", 37 | "not dead", 38 | "not op_mini all" 39 | ], 40 | "development": [ 41 | "last 1 chrome version", 42 | "last 1 firefox version", 43 | "last 1 safari version" 44 | ] 45 | }, 46 | "homepage": "." 47 | } 48 | -------------------------------------------------------------------------------- /src/streamlit_plotly_events/frontend/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Streamlit Component 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 |
13 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /src/streamlit_plotly_events/frontend/src/StreamlitPlotlyEventsComponent.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | Streamlit, 3 | StreamlitComponentBase, 4 | withStreamlitConnection, 5 | } from "streamlit-component-lib" 6 | import React, { ReactNode } from "react" 7 | import Plot from 'react-plotly.js'; 8 | 9 | class StreamlitPlotlyEventsComponent extends StreamlitComponentBase { 10 | public render = (): ReactNode => { 11 | // Pull Plotly object from args and parse 12 | const plot_obj = JSON.parse(this.props.args["plot_obj"]); 13 | const override_height = this.props.args["override_height"]; 14 | const override_width = this.props.args["override_width"]; 15 | 16 | // Event booleans 17 | const click_event = this.props.args["click_event"]; 18 | const select_event = this.props.args["select_event"]; 19 | const hover_event = this.props.args["hover_event"]; 20 | 21 | Streamlit.setFrameHeight(override_height); 22 | return ( 23 | 34 | ) 35 | } 36 | 37 | /** Click handler for plot. */ 38 | private plotlyEventHandler = (data: any) => { 39 | // Build array of points to return 40 | var clickedPoints: Array = []; 41 | data.points.forEach(function (arrayItem: any) { 42 | clickedPoints.push({ 43 | x: arrayItem.x, 44 | y: arrayItem.y, 45 | curveNumber: arrayItem.curveNumber, 46 | pointNumber: arrayItem.pointNumber, 47 | pointIndex: arrayItem.pointIndex 48 | }) 49 | }); 50 | 51 | // Return array as JSON to Streamlit 52 | Streamlit.setComponentValue(JSON.stringify(clickedPoints)) 53 | } 54 | } 55 | 56 | export default withStreamlitConnection(StreamlitPlotlyEventsComponent) 57 | -------------------------------------------------------------------------------- /src/streamlit_plotly_events/frontend/src/index.tsx: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import ReactDOM from "react-dom" 3 | import StreamlitPlotlyEventsComponent from "./StreamlitPlotlyEventsComponent" 4 | 5 | ReactDOM.render( 6 | 7 | 8 | , 9 | document.getElementById("root") 10 | ) 11 | -------------------------------------------------------------------------------- /src/streamlit_plotly_events/frontend/src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /src/streamlit_plotly_events/frontend/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "esModuleInterop": true, 8 | "allowSyntheticDefaultImports": true, 9 | "strict": true, 10 | "forceConsistentCasingInFileNames": true, 11 | "module": "esnext", 12 | "moduleResolution": "node", 13 | "resolveJsonModule": true, 14 | "isolatedModules": true, 15 | "noEmit": true, 16 | "jsx": "react" 17 | }, 18 | "include": ["src"] 19 | } 20 | --------------------------------------------------------------------------------