Streamlit components - Scatterplot with selection using Plotly.js
Tags: python, streamlit, plotly, datascience
Published: 2020-07-16 09:47:28

[Streamlit Components are out !](https://medium.com/streamlit/introducing-streamlit-components-d73f2092ae30) This opens a lot of new possibilities as it makes it easy to integrate any interactive Javascript library into Streamlit, and send data back and forth between a Python script and a React component. 

Let's build a quick example around it to help quickstart an answer [to a forum question about Plotly cross-interactive filtering](https://discuss.streamlit.io/t/could-we-have-plotly-figurewidget-support/4271) : can we build a scatterplot where selected points are sent back into Python ? We will solve this by creating a Plotly.js component with lasso selection and communicate selected points to Python. 

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/tucjnm38metahzgxemhe.gif)

*While this tutorial is accessible to anyone with no JS/React experience, I do skip on some important frontend/React concepts. I have a longer tutorial [here](https://streamlit-components-tutorial.netlify.app/) that addresses them but don't hesitate to check the React website if you want to go deeper.*

## Prerequisite : How does plotly work ?

A lot of Data Scientists use [plotly.py](https://plotly.com/python) to build interactive charts. plotly.py is actually a Python wrapper for [plotly.js](https://plotly.com/javascript/). Plotly figures are represented by a JSON specification which the Python client generates for you that plotly.js can consume to render a graph. 

If you know Altair from the Vega ecosystem, Altair works as a Python wrapper for Vega-lite in the exact same way plotly.py is a Python wrapper for plotly.js.

You can retrieve the JSON specification from a Figure in Python : 

```python
import plotly.express as px
fig = px.line(x=["a","b","c"], y=[1,3,2], title="sample figure")
fig.to_json()

# renders a somewhat big JSON spec
# {"data":[{"hovertemplate":"x=%{x}<br>y=%{y}<extra></extra>","legendgroup":"","line":{"color":"#636efa","dash":"solid"},"mode":"lines","name":"", ...
```

and copy this JSON spec in a Plotly.js element to see the rendered result. Here is [a Codepen for demonstration](https://codepen.io/andfanilo/pen/oNbaLNX).

> It's important to notice that a lot of JS charting libraries work by passing a JSON specification to them. Take a look at the documentation for echarts, Chart.js, Vega-lite, Victory ... 
> The takeaway here is this process can be replicated for integrating other charting libraries !

Our first task will be to replicate the `streamlit.plotly_charts` by passing the JSON representation of the plotly Figure to the Javascript library for rendering.

## Setup

Before pursuing this tutorial, you will need Python/Node.js installed on your system. 

First clone the [streamlit component-template]([https://github.com/streamlit/component-template](https://github.com/streamlit/component-template)), then move the template folder to your usual workspace. 

```bash
git clone https://github.com/streamlit/component-template
cp -r component-template/template ~/streamlit-custom-plotly
```

From now on we assume every following shell command is done from the `streamlit-custom-plotly` folder.

To pursue this, we need 2 terminals : one will run the Streamlit server, the other one a dev server containing the Javascript component. 

- In a terminal, install the frontend dependencies and then run the component server.

```bash
cd my_component/frontend
npm install    # Initialize the project and install npm dependencies
npm run start  # Start the Webpack dev server
```

- In another terminal, create a Python environment with Streamlit ≥ 0.63 and then run the `__init__.py` script

```bash
conda create -n streamlit-custom python=3 streamlit  # Create Conda env (or other Python environment manager)
conda activate streamlit-custom  # Activate environment
streamlit run my_component/__init__.py  # Run Streamlit app
```

You should see the Streamlit Hello world of Custom Components opening in a new browser : 

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/pgnd4qsxdw7ke889uo8o.png)

## Step 1 - "Hello world"

Head to your favorite code editor to edit the frontend code and only render "Hello world". Remove everything in `my_component/frontend/src/MyComponent.tsx` and paste the following code to render a single Hello world block : 

```jsx
import React, { useEffect } from "react"
import { withStreamlitConnection, Streamlit } from "./streamlit"

function MyComponent() {
  useEffect(() => Streamlit.setFrameHeight())

  return <p>Hello world</p>
}

export default withStreamlitConnection(MyComponent)
```

We should also clean the running Streamlit in `my_component/__init__.py` :

```python
import streamlit as st
import streamlit.components.v1 as components

_component_func = components.declare_component(
    "my_component",
    url="http://localhost:3001",
)

def my_component():
    return _component_func()

st.subheader("My Component")
my_component()
```

We're in Streamlit world, so livereload is enabled everywhere and the update in your browser should be instant.

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/4qkzqbtwlb1m9vsv6icx.png)

If we want to understand things a bit, on the Python side we have a wrapper `my_component()` function which calls a private `_component_func()`. This private function returned by `components.declare_component` accesses frontend resources from the url `http://localhost:3001`, the other running dev server delivering the frontend component !

On the Javascript side, we define a functional component `MyComponent` which returns a single block with "Hello world" inside. This is the output served by the dev server which Streamlit retrieves and renders in the browser. 

We also make use of a React hook `useEffect` which runs an anonymous callback function after the component has rendered to compute the height of the render, then update the height of the iframe containing the component. If you omit that function, your component will have a height of 0 and be invisible to the eye, but it will actually be there if you inspect the page source code with your browser's devtools.

## Step 2 - Bidirectional communication between Python and React

The `_component_func` in `my_component/__init__.py` manages the call to the frontend web server to fetch the component. Any argument passed through this function is JSON-serialized to the React component. For example a Python Dict is passed as a JSON object to the dev web server and available in our frontend counterpart. 

Time to add some parameters to the call :

```python
import streamlit as st
import streamlit.components.v1 as components

_component_func = components.declare_component(
    "my_component",
    url="http://localhost:3001",
)

def my_component():
    return _component_func(test="world") # <-- add some parameters in the call

st.subheader("My Component")
my_component()
```

And retrieve them on the React side in `my_component/frontend/src/MyComponent.tsx`

```jsx
import React, { useEffect } from "react"
import { withStreamlitConnection, Streamlit, ComponentProps } from "./streamlit"

// Your function has arguments now !
function MyComponent(props: ComponentProps) {
  useEffect(() => Streamlit.setFrameHeight())

  // Paramters from _component_func are stored in props.args
  return <p>Hello {props.args.test}</p>
}

export default withStreamlitConnection(MyComponent)
```

Livereload in your browser should make the update already visible !

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/r5nizkxop02rsg0jmu6n.png)

Here we set up data communication from Python to Javascript. To send data from Javascript to Python, we use the `Streamlit.setComponentValue()` method, the value will then be stored as the return value for `_component_func` :

```jsx
import React, { useEffect } from "react"
import { withStreamlitConnection, Streamlit, ComponentProps } from "./streamlit"

function MyComponent(props: ComponentProps) {
  useEffect(() => Streamlit.setFrameHeight())
  useEffect(() => Streamlit.setComponentValue(42)) // return value to Python after the component has rendered

  return <p>Hello {props.args.test}</p>
}

export default withStreamlitConnection(MyComponent)
```

Then on the Python side :

```python
import streamlit as st
import streamlit.components.v1 as components

_component_func = components.declare_component(
    "my_component",
    url="http://localhost:3001",
)

def my_component():
    return _component_func(test="test") # value from Streamlit.setComponentValue is now returned !

st.subheader("My Component")
v = my_component()
st.write(v)
```

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/nz3sz179sgcx80y4qxp4.png)

PS : you may see for a brief moment, while the component is being rendered, that `v = None` . You can change the default value returned by `_component_func` with the `default` parameter : `return _component_func(test="test", default=42)`

## Step 3 - Static Plotly.js plot

Time to spice things up, let's pass the JSON representation of our Plotly graph and render it with [react-plotly]([https://plotly.com/javascript/react/](https://plotly.com/javascript/react/)).

First install the frontend dependencies : 

```bash
cd my_component/frontend  # make sure you are running this from the frontend folder !
npm install react-plotly.js plotly.js @types/react-plotly.js
```

Test the installation using the [react-plotly quickstart code](https://plotly.com/javascript/react/#quick-start) in `my_component/frontend/src/MyComponent.tsx` :

```jsx
import React, { useEffect } from "react"
import { withStreamlitConnection, Streamlit, ComponentProps } from "./streamlit"
import Plot from "react-plotly.js"  // new dependency 

function MyComponent(props: ComponentProps) {
  useEffect(() => Streamlit.setFrameHeight())

  // we just changed the return value of the functional component to the one from https://plotly.com/javascript/react/#quick-start
  return (
    <Plot
      data={[
        {
          x: [1, 2, 3],
          y: [2, 6, 3],
          type: "scatter",
          mode: "lines+markers",
          marker: { color: "red" },
        },
        { type: "bar", x: [1, 2, 3], y: [2, 5, 3] },
      ]}
      layout={{ width: 400, height: 400, title: "A Fancy Plot" }}
    />
  )
}

export default withStreamlitConnection(MyComponent)
```

Be greeted by your plotly.js plot ! 

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/cku1yr3rr38hprzq3k6h.png)

_NB : I got a `FATAL ERROR: Ineffective mark-compacts near heap limit Allocation failed - JavaScript heap out of memory` there, which I solved with a `NODE_OPTIONS` variable [https://stackoverflow.com/questions/53230823/fatal-error-ineffective-mark-compacts-near-heap-limit-allocation-failed-javas](https://stackoverflow.com/questions/53230823/fatal-error-ineffective-mark-compacts-near-heap-limit-allocation-failed-javas)._

Now let's pass the Python figure and extract the JSON specification in the wrapper to push to the JS side. In `my_component/__init__.py` :

```python
import random
import plotly.express as px  
import streamlit as st
import streamlit.components.v1 as components

_component_func = components.declare_component(
    "my_component",
    url="http://localhost:3001",
)

def my_component(fig):
    return _component_func(spec=fig.to_json(), default=42)

st.subheader("My Component")
fig = px.scatter(x=random.sample(range(100), 50), y=random.sample(range(100), 50), title="My fancy plot")
v = my_component(fig)
st.write(v)
```

and then get it back on the Javascript side `my_component/frontend/src/MyComponent.tsx`

```jsx
import React, { useEffect } from "react"
import { withStreamlitConnection, Streamlit, ComponentProps } from "./streamlit"
import Plot from "react-plotly.js"

function MyComponent(props: ComponentProps) {
  useEffect(() => Streamlit.setFrameHeight())
  useEffect(() => Streamlit.setComponentValue(42))

  const { data, layout, frames, config } = JSON.parse(props.args.spec)

  return (
    <Plot
      data={data}
      layout={layout}
      frames={frames}
      config={config}
    />
  )
}

export default withStreamlitConnection(MyComponent)
```

Your plotly express plot should appear now : 

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/w36xxh0gjqlycnllrk5q.png)

> Why did we pass the JSON as a string to reparse it on the JS side ? I actually initially used `fig.to_dict` to pass a Dict in `_component_func`. It should then get serialized directly as a Javascript object, but it actually resulted in `Error serializing numpy.ndarray`, requiring converting the numpy array as Python lists so they can be considered JSON-serializable. I think there is a `plotly.utils.PlotlyJSONEncoder` that does it for you but haven't tested it...

If you rerender the app, Streamlit will recreate the plot from scratch with new data points. We can put the data in cache instead for our example. `_component_func` also has a `key` parameter which prevents the component from remounting from scratch when the Streamlit app rerenders : 

```python
import random
import plotly.express as px  
import streamlit as st
import streamlit.components.v1 as components

_component_func = components.declare_component(
    "my_component",
    url="http://localhost:3001",
)

def my_component(fig):
    # add key to _component_func so component is not destroyed/rebuilt on rerender
    return _component_func(spec=fig.to_json(), default=42, key="key")

# data in cache
@st.cache
def random_data():
    return random.sample(range(100), 50), random.sample(range(100), 50)

st.subheader("My Component")
x, y = random_data()
fig = px.scatter(x=x, y=y, title="My fancy plot")
v = my_component(fig)
st.write(v)
```

## Step 4 - Selection in Plotly.js

[How do we bind callback functions to lasso selection in plotly.js ?](https://plotly.com/javascript/lasso-selection/) Our interest is in using `Streamlit.setComponentValue()` inside this callback to return the selected data points back to Streamlit.

The page shows the callback event should be listened on `plotly_selected` , which has its own prop `onSelected` in [react-plotly](https://github.com/plotly/react-plotly.js/#event-handler-props). Define a handler to get back the information on selected points :

```jsx
import React, { useEffect } from "react"
import { withStreamlitConnection, Streamlit, ComponentProps } from "./streamlit"
import Plot from "react-plotly.js"

function MyComponent(props: ComponentProps) {
  useEffect(() => Streamlit.setFrameHeight())

  const handleSelected = function (eventData: any) {
    Streamlit.setComponentValue(
      eventData.points.map((p: any) => {
        return { index: p.pointIndex, x: p.x, y: p.y }
      })
    )
  }

  const { data, layout, frames, config } = JSON.parse(props.args.spec)

  return (
    <Plot
      data={data}
      layout={layout}
      frames={frames}
      config={config}
      onSelected={handleSelected}
    />
  )
}

export default withStreamlitConnection(MyComponent)
```

You could process the returned points in your Python wrapper to only return index or coordinates, depending on your usecase :

```python
import random
import plotly.express as px  
import streamlit as st
import streamlit.components.v1 as components

_component_func = components.declare_component(
    "my_component",
    url="http://localhost:3001",
)

def my_component(fig):
    points = _component_func(spec=fig.to_json(), default=[], key="key")
    return points

@st.cache
def random_data():
    return random.sample(range(100), 50), random.sample(range(100), 50)

st.subheader("My Component")
x, y = random_data()
fig = px.scatter(x=x, y=y, title="My fancy plot")
v = my_component(fig)
st.write(v)
```

You can now play with your plotly plot and get back selected data in Python :

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/0qkwy4tmu2gismuo5tvu.png)

There are still some problems here, for example there's no callback when unselecting the points using the `onDeselect` prop, I'll let that as an exercise :).

## Conclusion

Hope this small tutorial will help you start your new interactive charting library in Streamlit. You can quickly create very specific custom charts if you don't want to bother with a generic wrapper implementation, don't hesitate to play with this ! The plotly.js page even shows a cross-filter in multiple plots in the same component, so you can do your data manipulation in Python and extra visualization in Plotly.js. 

## Resources

* [Companion Code](https://github.com/andfanilo/streamlit_scatterplot_selection)