Click on a bar in bar plot to produce a scatterplot of the values in that bar

Viewed 102

This code produces a bar plot:

import numpy as np
import matplotlib.pyplot as plt
import pandas as pd



import plotly.graph_objects as go
classes= ['class1', 'class2', 'class3', 'class4', 'class5', 'class6', 'class7']
lens = [199, 30, 89, 59, 109, 115, 89]
nums = [145, 457, 123, 67, 35, 31, 134]


fig = go.Figure(data=[
    go.Bar(name='Length', x=classes, y=lens),
    go.Bar(name='Number', x=classes, y=nums),
])

# Change the bar mode
fig.update_layout(barmode='group')
fig.update_layout(title_text='Length and Number',
                  title_x=0.1,
                  plot_bgcolor='rgba(0,0,0,0)',
                  paper_bgcolor='rgba(0,0,0,0)',
                  bargap=0.30,
                  bargroupgap=0.0,
                  margin=dict(l=50, r=50, t=50, b=50),
                  xaxis_title="Score Class",
                  yaxis_title="Length and Number",
                  yaxis = dict(
                  tickfont = dict(size=13)),

                  xaxis = dict(
                  tickfont = dict(size=13)),)



fig.update_xaxes(showline=True, linewidth=2, linecolor='black')
fig.update_yaxes(showline=True, linewidth=2, linecolor='black')
fig.show()

The output is:

enter image description here

I want to click on any of the red bars, and it will bring me to a scatterplot of values in that class.

I can produce a scatterplot with this:

dict2 = {}

dict2['class1'] = [(2,2),(1,1),(2,3),(3,4),(5,1)]
dict2['class2'] = [(3,1),(4,4),(5,5),(6,2),(7,1)]
dict2['class3'] = [(3,2),(4,1),(5,4),(6,4),(7,1)]
dict2['class4'] = [(3,1),(4,5),(6,3),(4,3),(5,3)]
dict2['class5'] = [(1,1),(1,1),(1,2),(3,1),(4,3)]
dict2['class6'] = [(2,2),(2,1),(2,3),(5,3),(6,4)]


class1_dict = {}
class1_dict['xs'] = [i[0] for i in dict2['class1']]
class1_dict['ys'] = [i[1] for i in dict2['class1']]
plt.scatter(class1_dict['xs'],class1_dict['ys'])
plt.show()

enter image description here

And I know how to click on a bar generally to return a dataframe that I could put into the scatterplot like this:

dict_name = {}
dict_name['classes'] = classes
dict_name['lens'] = lens
dict_name['nums'] = nums

df = pd.DataFrame.from_dict(dict_name, orient='columns')
print(df)
axs = df.hist(bins=4, picker=True)
ax = axs[0, 0]

def onpick(event):
    bar = event.artist
    left = bar.get_x()
    right = left + bar.get_width()
    col_df = df[(df.lens >= left) & (df.lens <= right)]
    
    
ax.figure.canvas.mpl_connect('pick_event', onpick)
#plt.show()

I'm trying to change that last piece of code, so instead of axs = df.hist(bins=4, picker=True), I can read in my bar plot, and upon clicking, return a dataframe that I can read into a scatterplot.

So I thought I just needed to somehow add these two lines:

axs = df.hist(bins=4, picker=True)
ax = axs[0, 0]

To my bar plot code, to make it clickable.

So I thought since axs is just a plot, which is what fig, I could just add this line to the bar plot code and it would work:

fig = go.Figure(data=[
    go.Bar(name='Length', x=classes, y=lens),
    go.Bar(name='Number', x=classes, y=nums),
]) 
ax = fig[0,0]

The error I get is:

Traceback (most recent call last):
  File "/Users/slowatkela/anaconda/lib/python3.7/site-packages/plotly/basedatatypes.py", line 188, in _check_path_in_prop_tree
    obj = obj[p]
  File "/Users/slowatkela/anaconda/lib/python3.7/site-packages/plotly/basedatatypes.py", line 732, in __getitem__
    prop = BaseFigure._str_to_dict_path(prop)
  File "/Users/slowatkela/anaconda/lib/python3.7/site-packages/plotly/basedatatypes.py", line 1839, in _str_to_dict_path
    ret = _str_to_dict_path_full(key_path_str)[0]
  File "/Users/slowatkela/anaconda/lib/python3.7/site-packages/plotly/basedatatypes.py", line 71, in _str_to_dict_path_full
    if len(key_path_str):
TypeError: object of type 'int' has no len()

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "test3.py", line 17, in <module>
    ax=axs[0,0]
  File "/Users/slowatkela/anaconda/lib/python3.7/site-packages/plotly/basedatatypes.py", line 754, in __getitem__
    err = _check_path_in_prop_tree(self, orig_prop, error_cast=PlotlyKeyError)
  File "/Users/slowatkela/anaconda/lib/python3.7/site-packages/plotly/basedatatypes.py", line 212, in _check_path_in_prop_tree
    if prop[i][0] == "_":
TypeError: 'int' object is not subscriptable

I guess it's because the first plot makes a grouped bar plot makes one figure whereas the histogram example makes two plots? Could someone show me where I'm going wrong?

2 Answers

You're not specifying that you'd like to combine plotly and matplotlib in any way, so in case you'd like to know how to do exactly what you're asking for with Plotly, here's a setup for that. I'd be happy to explain the details if this is something you can use.

Plotly Dash App

enter image description here

Complete code:

import plotly.graph_objects as go # or plotly.express as px
fig = go.Figure() 

from jupyter_dash import JupyterDash
import dash
import dash_core_components as dcc
import dash_html_components as html
from dash.dependencies import Input, Output
import json


import plotly.express as px

styles = {
    'pre': {
        'border': 'thin lightgrey solid',
        'overflowX': 'scroll'
    }
}


df = px.data.tips()
fig1 = px.bar(df, x="sex", y="total_bill", color='time')
fig2= px.scatter(df, x="total_bill", y="tip")
# f = fig.full_figure_for_development(warn=False)

app = JupyterDash(__name__)

app.layout = html.Div([
    dcc.Graph(id = 'fig1', figure=fig1),
    dcc.Graph(id = 'fig2', figure=fig2),
    html.Div(className='row', children=[
        html.Div([
            dcc.Markdown(("""
              Clickinfo:
            """)),
            html.Pre(id='txt_output', style=styles['pre']),
        ], className='three columns'),
    ])
])

# inspect clickdata
@app.callback(
    Output('txt_output', 'children'),
    [Input('fig1', 'clickData')])
def display_click_data(clickData):
    if clickData is not None:
        output = json.dumps({'clickinfo':clickData}
                                , indent = 2)
        return output

# Use clickrInfo from fig1 to subset data in fig2
@app.callback(
    Output('fig2', 'figure'),
    [Input('fig1', 'clickData')])
def display_click_data(clickData):
    
    if clickData is not None:
        subset = clickData['points'][0]['x']
        fig = px.scatter(df[df['sex'] == subset], x="total_bill", y="tip")
        return fig
    return fig2


app.run_server(mode='external', port = 8071, dev_tools_ui=True,
          dev_tools_hot_reload =True, threaded=True)

As mentioned by @JohanC in the comments, plotly and matplotlib are very different libraries. This means that their objects are not related by any kind of class hierarchy, and don't share the same properties.

For that reason, you cannot set a matplotlib axes object equal to a plotly figure object. A plotly figure object is not the same as a matplotlib figure object. You will probably need to stay within one library to achieve what you want. If the matplotlib onpick functionality is important to you, then you should probably stay within matplotlib. I believe that in matplotlib you can construct hoverevents but it's more effort than in plotly which has hoverevents as the default for almost if not all of the figures.

Also a plotly figure isn't an array so fig[0,0] doesn't make sense. Perhaps you meant to access fig.data which is a tuple meaning you can access fig.data[0], fig.data[1], ... fig.data[N]

Related