Skip to content

Commit

Permalink
Move directories around & prep for first release
Browse files Browse the repository at this point in the history
  • Loading branch information
null-jones committed Apr 20, 2021
1 parent 0dde436 commit d4c0fd1
Show file tree
Hide file tree
Showing 15 changed files with 224 additions and 92 deletions.
2 changes: 1 addition & 1 deletion MANIFEST.in
Original file line number Diff line number Diff line change
@@ -1 +1 @@
recursive-include package/frontend/build *
recursive-include src/streamlit_plotly_events/frontend/build *
87 changes: 86 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,89 @@
# 🔥📊📣 Streamlit Plotly Events 📣📊🔥
Ever wanted to harness those awesome events from Plotly charts inside of Streamlit? So did I, so now you can!

This is relatively trivial to accomplish, so I'd bet good money Streamlit is already working on this, but I can't wait :)
![Example Image](example.gif)

## Overview, TL;DR
### Installation
Install via Pip!

```pip install streamlit-plotly-events```

### Usage
Import the component, and use it like any other Streamlit custom component!
```python
import streamlit as st
from streamlit_plotly_events import plotly_events

# Writes a component similar to st.write()
fig = px.line(x=[1], y=[1])
selected_points = plotly_events(fig)

# Can write inside of things using with!
with st.beta_expander('Plot'):
fig = px.line(x=[1], y=[1])
selected_points = plotly_events(fig)

# Select other Plotly events by specifying kwargs
fig = px.line(x=[1], y=[1])
selected_points = plotly_events(fig, click_event=False, hover_event=True)
```

What the component returns:
```
Returns
-------
list of dict
List of dictionaries containing point details (in case multiple overlapping points have been clicked).
Details can be found here:
https://plotly.com/javascript/plotlyjs-events/#event-data
Format of dict:
{
x: int (x value of point),
y: int (y value of point),
curveNumber: (index of curve),
pointNumber: (index of selected point),
pointIndex: (index of selected point)
}
```

## Events
Currently, a number of plotly events can be enabled. They can be enabled/disabled using kwargs on the `plotly_event()` function.
- **Click** `click_event` (defaults to `True`): Triggers event on mouse click of point
- **Select** `select_event`: Triggers event when points have been lasso
- **Hover** `hover_event`: Triggers event on mouse hover of point (**WARNING: VERY RESOURCE INTENSIVE**)

# Contributing
Please! I'm hardly a frontend developer! I think there's a bunch of amazing functionality we can add into streamlit/plotly!!

This repo follows `black` formatting standards for the Python parts of the project.

Follow the instructions on the `streamlit_components` [example repository](https://github.com/streamlit/component-template) to get up and running, or follow along below!

### Quickstart

* 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.
* Clone this repo.
* Create a new Python virtual environment for the template:
```
$ cd template
$ python3 -m venv venv # create venv
$ . venv/bin/activate # activate venv
$ pip install streamlit # install streamlit
$ pip install plotly # install plotly
```
* Initialize and run the component template frontend:
```
$ cd src/streamlit_plotly_events/frontend
$ npm install # Install npm dependencies
$ npm run start # Start the Webpack dev server
```
* From a separate terminal, run the template's Streamlit app:
```
$ cd src/streamlit_plotly_events
$ . venv/bin/activate # activate the venv you created earlier
$ streamlit run __init__.py # run the example server
```
Binary file added example.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
9 changes: 5 additions & 4 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,15 @@

setuptools.setup(
name="streamlit-plotly-events",
version="0.0.1",
version="0.0.5",
author="Ellie Jones",
author_email="[email protected]",
description="Plotly chart component for Streamlit that also allows for events to bubble back up to Streamlit.",
long_description="",
long_description="Plotly chart component for Streamlit that also allows for events to bubble back up to Streamlit.",
long_description_content_type="text/plain",
url="",
packages=setuptools.find_packages(),
url="https://github.com/null-jones/streamlit-plotly-events",
package_dir={"": "src"},
packages=setuptools.find_packages(where="src"),
include_package_data=True,
classifiers=[],
python_requires=">=3.6",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import os
import streamlit.components.v1 as components
from json import loads

# Create a _RELEASE constant. We'll set this to False while we're developing
# the component, and True when we're ready to package and distribute it.
Expand Down Expand Up @@ -43,59 +44,98 @@
# `declare_component` and call it done. The wrapper allows us to customize
# our component's API: we can pre-process its input args, post-process its
# output value, and add a docstring for users.
def plotly_events(plot_fig):
def plotly_events(
plot_fig,
click_event=True,
select_event=False,
hover_event=False,
override_height=450,
key=None,
):
"""Create a new instance of "plotly_events".
Parameters
----------
name: str
The name of the thing we're saying hello to. The component will display
the text "Hello, {name}!"
plot_fig: Plotly Figure
Plotly figure that we want to render in Streamlit
click_event: boolean, default: True
Watch for click events on plot and return point data when triggered
select_event: boolean, default: False
Watch for select events on plot and return point data when triggered
hover_event: boolean, default: False
Watch for hover events on plot and return point data when triggered
override_height: int, default: 450
Integer to override component height. Defaults to 450 (px)
key: str or None
An optional key that uniquely identifies this component. If this is
None, and the component's arguments are changed, the component will
be re-mounted in the Streamlit frontend and lose its current state.
Returns
-------
int
The number of times the component's "Click Me" button has been clicked.
(This is the value passed to `Streamlit.setComponentValue` on the
frontend.)
list of dict
List of dictionaries containing point details (in case multiple overlapping
points have been clicked).
Details can be found here:
https://plotly.com/javascript/plotlyjs-events/#event-data
Format of dict:
{
x: int (x value of point),
y: int (y value of point),
curveNumber: (index of curve),
pointNumber: (index of selected point),
pointIndex: (index of selected point)
}
"""
# Call through to our private component function. Arguments we pass here
# will be sent to the frontend, where they'll be available in an "args"
# dictionary.
#
# "default" is a special argument that specifies the initial return
# value of the component before the user has interacted with it.
component_value = _component_func(plot_obj=fig.to_json(), default=False)
# kwargs will be exposed to frontend in "args"
component_value = _component_func(
plot_obj=plot_fig.to_json(),
override_height=override_height,
key=key,
click_event=click_event,
select_event=select_event,
hover_event=hover_event,
default="[]", # Default return empty JSON list
)

# We could modify the value returned from the component if we wanted.
# There's no need to do this in our simple example - but it's an option.
return component_value
# Parse component_value since it's JSON and return to Streamlit
return loads(component_value)


# Add some test code to play with the component while it's in development.
# During development, we can run this just as we would any other Streamlit
# app: `$ streamlit run my_component/__init__.py`
# app: `$ streamlit run src/streamlit_plotly_events/__init__.py`
if not _RELEASE:
import streamlit as st
import plotly.express as px
st.subheader("Component with variable args")

# Create a second instance of our component whose `name` arg will vary
# based on a text_input widget.
#
# We use the special "key" argument to assign a fixed identity to this
# component instance. By default, when a component's arguments change,
# it is considered a new instance and will be re-mounted on the frontend
# and lose its current state. In this case, we want to vary the component's
# "name" argument without having it get recreated.
name_input = st.text_input("Enter a name", value="Streamlit")

st.subheader("Plotly Line Chart")
fig = px.line(x=[0, 1, 2, 3], y=[0, 1, 2, 3])
plot_name_holder = st.empty()
clickedPoint = plotly_events(fig)
plot_name_holder.write(f"Selected Point: {clickedPoint}")
# st.markdown("You've clicked %s times!" % int(num_clicks))
clickedPoint = plotly_events(fig, key="line")
plot_name_holder.write(f"Clicked Point: {clickedPoint}")

st.subheader("Plotly Bar Chart")
fig2 = px.bar(x=[0, 1, 2, 3], y=[0, 1, 2, 3])
plot_name_holder2 = st.empty()
clickedPoint2 = plotly_events(fig2, key="bar")
plot_name_holder2.write(f"Clicked Point: {clickedPoint2}")

st.subheader("# Plotly Select Event")
fig3 = px.bar(x=[0, 1, 2, 3], y=[0, 1, 2, 3])
plot_name_holder3 = st.empty()
clickedPoint3 = plotly_events(
fig3, key="select", click_event=False, select_event=True
)
plot_name_holder3.write(f"Selected Point: {clickedPoint3}")

st.subheader("# Plotly Hover Event")
fig4 = px.bar(x=[0, 1, 2, 3], y=[0, 1, 2, 3])
plot_name_holder4 = st.empty()
clickedPoint4 = plotly_events(
fig4, key="hover", click_event=False, hover_event=True
)
plot_name_holder4.write(f"Hovered Point: {clickedPoint4}")
File renamed without changes.
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,13 @@
"@types/node": "^12.0.0",
"@types/react": "^16.9.0",
"@types/react-dom": "^16.9.0",
"plotly.js": "^1.58.2",
"react": "^16.13.1",
"react-dom": "^16.13.1",
"react-plotly.js": "^2.4.0",
"react-scripts": "3.4.1",
"streamlit-component-lib": "^1.2.0",
"typescript": "~3.7.2",
"plotly.js": "^1.58.2",
"react-plotly.js": "^2.4.0"
"typescript": "~3.7.2"
},
"devDependencies": {
"@types/plotly.js": "^1.50.16",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import {
Streamlit,
StreamlitComponentBase,
withStreamlitConnection,
} from "streamlit-component-lib"
import React, { ReactNode } from "react"
import Plot from 'react-plotly.js';

class StreamlitPlotlyEventsComponent extends StreamlitComponentBase {
public render = (): ReactNode => {
// Pull Plotly object from args and parse
const plot_obj = JSON.parse(this.props.args["plot_obj"]);
const override_height = this.props.args["override_height"];

// Event booleans
const click_event = this.props.args["click_event"];
const select_event = this.props.args["select_event"];
const hover_event = this.props.args["hover_event"];

Streamlit.setFrameHeight(override_height);
return (
<div
style={{"height": override_height}}
className="stPlotlyChart"
>
<Plot
data={plot_obj.data}
layout={plot_obj.layout}
onClick={click_event ? this.plotlyEventHandler : function(){}}
onSelected={select_event ? this.plotlyEventHandler : function(){}}
onHover={hover_event ? this.plotlyEventHandler : function(){}}
/>
</div>
)
}

/** Click handler for plot. */
private plotlyEventHandler = (data: any) => {
// Build array of points to return
var clickedPoints: Array<any> = [];
data.points.forEach(function (arrayItem: any) {
clickedPoints.push({
x: arrayItem.x,
y: arrayItem.y,
curveNumber: arrayItem.curveNumber,
pointNumber: arrayItem.pointNumber,
pointIndex: arrayItem.pointIndex
})
});

// Return array as JSON to Streamlit
Streamlit.setComponentValue(JSON.stringify(clickedPoints))
}
}

export default withStreamlitConnection(StreamlitPlotlyEventsComponent)

This file was deleted.

0 comments on commit d4c0fd1

Please sign in to comment.