├── MANIFEST.in ├── streamlit_advanced_plotly_chart ├── frontend │ ├── src │ │ ├── react-app-env.d.ts │ │ ├── index.tsx │ │ └── PlotlyPreserveZoomComponent.tsx │ ├── .prettierrc │ ├── .env │ ├── tsconfig.json │ ├── public │ │ └── index.html │ └── package.json └── __init__.py ├── setup.py ├── LICENSE ├── README.md └── .gitignore /MANIFEST.in: -------------------------------------------------------------------------------- 1 | recursive-include streamlit_advanced_plotly_chart/frontend/build * 2 | -------------------------------------------------------------------------------- /streamlit_advanced_plotly_chart/frontend/src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /streamlit_advanced_plotly_chart/frontend/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "endOfLine": "lf", 3 | "semi": false, 4 | "trailingComma": "es5" 5 | } 6 | -------------------------------------------------------------------------------- /streamlit_advanced_plotly_chart/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_advanced_plotly_chart/frontend/src/index.tsx: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import ReactDOM from "react-dom" 3 | import PlotlyPreserveZoomComponent from "./PlotlyPreserveZoomComponent" 4 | 5 | ReactDOM.render( 6 | 7 | 8 | , 9 | document.getElementById("root") 10 | ) 11 | -------------------------------------------------------------------------------- /streamlit_advanced_plotly_chart/frontend/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": [ 5 | "dom", 6 | "dom.iterable", 7 | "esnext" 8 | ], 9 | "allowJs": true, 10 | "skipLibCheck": true, 11 | "esModuleInterop": true, 12 | "allowSyntheticDefaultImports": true, 13 | "strict": true, 14 | "forceConsistentCasingInFileNames": true, 15 | "module": "esnext", 16 | "moduleResolution": "node", 17 | "resolveJsonModule": true, 18 | "isolatedModules": true, 19 | "noEmit": true, 20 | "jsx": "react-jsx", 21 | "noFallthroughCasesInSwitch": true 22 | }, 23 | "include": [ 24 | "src" 25 | ] 26 | } 27 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import setuptools 2 | 3 | setuptools.setup( 4 | name="streamlit-advanced-plotly-chart", 5 | version="0.0.2", 6 | author="Fabian Grob", 7 | author_email="grobfab@gmail.com", 8 | description="Custom Streamlit component to preserve zoom level of plotly charts when getting event data from them.", 9 | long_description=open("README.md").read(), 10 | long_description_content_type="text/plain", 11 | url="https://github.com/fabianandresgrob/streamlit-advanced-plotly-chart", 12 | packages=setuptools.find_packages(), 13 | include_package_data=True, 14 | classifiers=[], 15 | python_requires=">=3.6", 16 | install_requires=[ 17 | # By definition, a Custom Component depends on Streamlit. 18 | # If your component has other Python dependencies, list 19 | # them here. 20 | "streamlit >= 0.63", 21 | ], 22 | ) 23 | -------------------------------------------------------------------------------- /streamlit_advanced_plotly_chart/frontend/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Streamlit Component 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 |
14 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2018-2021 Streamlit Inc. 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # streamlit-advanced-plotly-chart 2 | This is a streamlit custom component to preserve the zoom level of a plotly chart when getting event data from it. It also returns the range of the x- and y-axis, which is useful for zooming in on a specific part of the chart and using a rangeslider. 3 | 4 | It is based on the [streamlit-plotly-events](https://github.com/null-jones/streamlit-plotly-events/tree/master) component. 5 | 6 | 7 | ## Usage 8 | ```python 9 | import streamlit as st 10 | import plotly.express as px 11 | import numpy as np 12 | 13 | st.set_page_config(layout="wide") 14 | 15 | st.subheader("Plotly Line Chart") 16 | 17 | time = np.arange(0, 100, 0.1) 18 | amplitude = np.sin(time) 19 | 20 | fig = px.line(x=time, y=amplitude) 21 | 22 | fig.update_xaxes(rangeslider_visible=True, range=[0, 10]) 23 | 24 | clickedPoint = preserveZoomPlotlyChart(fig, event='click') 25 | ``` 26 | 27 | When adding multiple lines to the chart and using a key, the plotly chart is stuck. Therefore, the key argument is not supported. 28 | 29 | ## Installation 30 | ```bash 31 | pip install streamlit-advanced-plotly-chart 32 | ``` 33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /streamlit_advanced_plotly_chart/frontend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "streamlit_component_template", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@types/jest": "^26.0.14", 7 | "@types/node": "^18.15.3", 8 | "@types/react": "^17.0.2", 9 | "@types/react-dom": "^18.0.11", 10 | "@types/react-plotly.js": "^2.6.0", 11 | "plotly.js": "^2.19.1", 12 | "react": "^18.2.0", 13 | "react-dom": "^18.2.0", 14 | "react-plotly.js": "^2.6.0", 15 | "react-scripts": "4.0.3", 16 | "streamlit-component-lib": "^1.3.0", 17 | "typescript": "^4.9.5" 18 | }, 19 | "scripts": { 20 | "start": "react-scripts start", 21 | "build": "react-scripts build", 22 | "test": "react-scripts test", 23 | "eject": "react-scripts eject" 24 | }, 25 | "eslintConfig": { 26 | "extends": "react-app" 27 | }, 28 | "browserslist": { 29 | "production": [ 30 | ">0.2%", 31 | "not dead", 32 | "not op_mini all" 33 | ], 34 | "development": [ 35 | "last 1 chrome version", 36 | "last 1 firefox version", 37 | "last 1 safari version" 38 | ] 39 | }, 40 | "homepage": ".", 41 | "devDependencies": { 42 | "@types/lodash.debounce": "^4.0.7" 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__/ 2 | *.py[cod] 3 | *$py.class 4 | 5 | # Distribution / packaging 6 | build/ 7 | dist/ 8 | eggs/ 9 | .eggs/ 10 | *.egg-info/ 11 | *.egg 12 | 13 | # Unit test / coverage reports 14 | .coverage 15 | .coverage\.* 16 | .pytest_cache/ 17 | .mypy_cache/ 18 | test-reports 19 | 20 | # Test fixtures 21 | cffi_bin 22 | 23 | # Pyenv Stuff 24 | .python-version 25 | 26 | ######################################################################## 27 | # OSX - https://github.com/github/gitignore/blob/master/Global/macOS.gitignore 28 | ######################################################################## 29 | .DS_Store 30 | .DocumentRevisions-V100 31 | .fseventsd 32 | .Spotlight-V100 33 | .TemporaryItems 34 | .Trashes 35 | .VolumeIcon.icns 36 | .com.apple.timemachine.donotpresent 37 | 38 | ######################################################################## 39 | # node - https://github.com/github/gitignore/blob/master/Node.gitignore 40 | ######################################################################## 41 | # Logs 42 | npm-debug.log* 43 | yarn-debug.log* 44 | yarn-error.log* 45 | 46 | # Dependency directories 47 | node_modules/ 48 | 49 | # Coverage directory used by tools like istanbul 50 | coverage/ 51 | 52 | # Lockfiles 53 | yarn.lock 54 | package-lock.json 55 | 56 | ######################################################################## 57 | # JetBrains 58 | ######################################################################## 59 | .idea 60 | 61 | ######################################################################## 62 | # VSCode 63 | ######################################################################## 64 | .vscode/ 65 | 66 | 67 | # Virtual Env 68 | env/* -------------------------------------------------------------------------------- /streamlit_advanced_plotly_chart/frontend/src/PlotlyPreserveZoomComponent.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | Streamlit, 3 | withStreamlitConnection, 4 | ComponentProps, 5 | } from "streamlit-component-lib" 6 | import Plot from 'react-plotly.js' 7 | import { useState } from "react" 8 | 9 | const PlotlyPreserveZoomComponent = (props: ComponentProps): any => { 10 | // Pull Plotly object from args and parse 11 | const { data, layout, frames, config } = JSON.parse(props.args.spec); 12 | const override_height = props.args.override_height; 13 | const override_width = props.args.override_width; 14 | 15 | // Initialize events 16 | let click_event = false; 17 | let select_event = false; 18 | let hover_event = false; 19 | 20 | // Get Event and set according events to false/true with switch 21 | const event = props.args.event; 22 | switch (event) { 23 | case "click": 24 | click_event = true; 25 | break; 26 | case "select": 27 | select_event = true; 28 | break; 29 | case "hover": 30 | hover_event = true; 31 | break; 32 | } 33 | 34 | /** Click handler for plot. */ 35 | const plotlyEventHandler = (eventData: any) => { 36 | // If no event data, send only the current range to Streamlit 37 | if (eventData === null) { 38 | const range_x = state.layout.xaxis.range; 39 | const range_y = state.layout.yaxis.range; 40 | const clickedPointsDict = { 41 | points: [], 42 | selected_range_x: range_x, 43 | selected_range_y: range_y 44 | } 45 | Streamlit.setComponentValue(clickedPointsDict); 46 | return; 47 | } 48 | // Build array of points to return 49 | var clickedPoints: Array = []; 50 | eventData.points.forEach(function (arrayItem: any) { 51 | clickedPoints.push({ 52 | x: arrayItem.x, 53 | y: arrayItem.y, 54 | curveNumber: arrayItem.curveNumber, 55 | pointNumber: arrayItem.pointNumber, 56 | pointIndex: arrayItem.pointIndex 57 | }) 58 | }); 59 | const range_x = state.layout.xaxis.range; 60 | const range_y = state.layout.yaxis.range; 61 | 62 | // build dict to return 63 | const clickedPointsDict = { 64 | points: clickedPoints, 65 | selected_range_x: range_x, 66 | selected_range_y: range_y 67 | } 68 | 69 | // Send event to Streamlit 70 | Streamlit.setComponentValue(clickedPointsDict); 71 | } 72 | // Preserve zoom etc. state 73 | const [state, setState] = useState({data, layout, frames, config}); 74 | 75 | Streamlit.setFrameHeight(override_height); 76 | 77 | return ( 78 | { 89 | setState( 90 | { 91 | data: data, 92 | layout: layout, 93 | frames: frames, 94 | config: config 95 | } 96 | ) 97 | plotlyEventHandler(null); 98 | } 99 | } 100 | onUpdate={( 101 | figure: any, 102 | ) => { 103 | console.log("onUpdate") 104 | console.log(state.data) 105 | setState( 106 | { 107 | data: figure.data, 108 | layout: figure.layout, 109 | frames: figure.frames, 110 | config: figure.config 111 | } 112 | ) 113 | } 114 | } 115 | style={{width: override_width, height: override_height}} 116 | onAfterPlot={() => { 117 | plotlyEventHandler(null); 118 | }} 119 | /> 120 | ) 121 | } 122 | 123 | export default withStreamlitConnection(PlotlyPreserveZoomComponent) 124 | -------------------------------------------------------------------------------- /streamlit_advanced_plotly_chart/__init__.py: -------------------------------------------------------------------------------- 1 | import os 2 | import streamlit.components.v1 as components 3 | import json 4 | import plotly.utils 5 | 6 | 7 | # Create a _RELEASE constant. We'll set this to False while we're developing 8 | # the component, and True when we're ready to package and distribute it. 9 | # (This is, of course, optional - there are innumerable ways to manage your 10 | # release process.) 11 | _RELEASE = True 12 | 13 | # Declare a Streamlit component. `declare_component` returns a function 14 | # that is used to create instances of the component. We're naming this 15 | # function "_component_func", with an underscore prefix, because we don't want 16 | # to expose it directly to users. Instead, we will create a custom wrapper 17 | # function, below, that will serve as our component's public API. 18 | 19 | # It's worth noting that this call to `declare_component` is the 20 | # *only thing* you need to do to create the binding between Streamlit and 21 | # your component frontend. Everything else we do in this file is simply a 22 | # best practice. 23 | 24 | if not _RELEASE: 25 | _component_func = components.declare_component( 26 | "streamlit_advanced_plotly_chart", 27 | # Pass `url` here to tell Streamlit that the component will be served 28 | # by the local dev server that you run via `npm run start`. 29 | # (This is useful while your component is in development.) 30 | url="http://localhost:3001", 31 | ) 32 | else: 33 | # When we're distributing a production version of the component, we'll 34 | # replace the `url` param with `path`, and point it to to the component's 35 | # build directory: 36 | parent_dir = os.path.dirname(os.path.abspath(__file__)) 37 | build_dir = os.path.join(parent_dir, "frontend/build") 38 | _component_func = components.declare_component("streamlit_advanced_plotly_chart", path=build_dir) 39 | 40 | 41 | # Create a wrapper function for the component. This is an optional 42 | # best practice - we could simply expose the component function returned by 43 | # `declare_component` and call it done. The wrapper allows us to customize 44 | # our component's API: we can pre-process its input args, post-process its 45 | # output value, and add a docstring for users. 46 | def preserveZoomPlotlyChart( 47 | plot_fig, 48 | event="click", 49 | override_height=450, 50 | override_width="100%" 51 | ): 52 | """Create a new instance of "plotly_events". 53 | Parameters 54 | ---------- 55 | plot_fig: Plotly Figure 56 | Plotly figure that we want to render in Streamlit 57 | event: string, default: 'click' 58 | Event to watch for. Can be 'click', 'select', or 'hover' 59 | override_height: int, default: 450 60 | Integer to override component height. Defaults to 450 (px) 61 | override_width: string, default: '100%' 62 | String (or integer) to override width. Defaults to 100% (whole width of iframe) 63 | key: str or None 64 | An optional key that uniquely identifies this component. If this is 65 | None, and the component's arguments are changed, the component will 66 | be re-mounted in the Streamlit frontend and lose its current state. 67 | Returns 68 | ------- 69 | list of dict 70 | List of dictionaries containing point details (in case multiple overlapping 71 | points have been clicked). 72 | Details can be found here: 73 | https://plotly.com/javascript/plotlyjs-events/#event-data 74 | Format of dict: 75 | { 76 | points: list of dict: 77 | { 78 | x: int (x value of point), 79 | y: int (y value of point), 80 | curveNumber: (index of curve), 81 | pointNumber: (index of selected point), 82 | pointIndex: (index of selected point), 83 | } 84 | selected_range_x: list [min_x, max_x], 85 | selected_range_y: list [min_y, max_y] 86 | } 87 | """ 88 | # kwargs will be exposed to frontend in "args" 89 | spec = json.dumps(plot_fig, cls=plotly.utils.PlotlyJSONEncoder) 90 | component_value = _component_func( 91 | spec=spec, 92 | override_height=override_height, 93 | override_width=override_width, 94 | event=event 95 | ) 96 | 97 | # Parse component_value since it's JSON and return to Streamlit 98 | return component_value 99 | 100 | 101 | # Add some test code to play with the component while it's in development. 102 | # During development, we can run this just as we would any other Streamlit 103 | # app: `$ streamlit run my_component/__init__.py` 104 | if not _RELEASE: 105 | import streamlit as st 106 | import plotly.express as px 107 | import numpy as np 108 | 109 | st.set_page_config(layout="wide") 110 | 111 | st.subheader("Plotly Line Chart") 112 | 113 | time = np.arange(0, 100, 0.1) 114 | amplitude = np.sin(time) 115 | 116 | fig = px.line(x=time, y=amplitude) 117 | 118 | fig.update_xaxes(rangeslider_visible=True, range=[0, 10]) 119 | 120 | clickedPoint = preserveZoomPlotlyChart(fig, event='click') 121 | 122 | st.write(f"Clicked Point: {clickedPoint}") 123 | --------------------------------------------------------------------------------