├── 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 |
--------------------------------------------------------------------------------