How to correctly use the buttons for filtering data in Plotly

Viewed 1331

I have a dataframe that looks like this where the 'dni' is a unique id for each row. I want to use plotly for a stacked barplot across the dates column for each individual dni.

dni date col1 col2 col3
unique ids Datetime 5 7 1

The code I'm currently using looks like this:

fig = go.Figure()
buttons = []

for i,dni in enumerate(sorted(df_merged.dni.unique())):
  df = df_merged[df_merged['dni']==dni]
  for column in df.columns[3:-1]:
      fig.add_trace(go.Bar(
                        name = column,
                        x = pd.to_datetime(df.date.astype('str')),
                        y = df[column], 
                        visible = (i==0)
                      ))
  args = [False] * df_merged.dni.nunique()
  args[i] = True
    
  button = dict(label = dni,
                method = "update",
                args=[{"visible": args}])
      
  buttons.append(button)
        
fig.update_layout(
    updatemenus=[
        dict(
        type="dropdown",
        direction="down",
        buttons = buttons)
    ],
    barmode = "stack",)
fig.show()

This does give me a plot with filters but the data it shows is always incorrect. I'm struggling to understand where I'm going wrong. The only correct data it shows is for the first dni which also gets changed when I actually press the button. Rendered plotly graph

Thanks!

EDIT: This is the actual data:

           id      date  bills  goalTrans  incomes  payments  savings
0  12345678901  2020-12    1.0        2.0      1.0       0.0      0.0
1  23456789012  2021-02    6.0        0.0      2.0       0.0      0.0
2  34567890123  2020-12    4.0        0.0      2.0       0.0      0.0
3  45678901234  2020-12    9.0        1.0      1.0       0.0      0.0
4  56789012345  2021-01    3.0        0.0      2.0       1.0      0.0

{'bills': {0: 1.0, 1: 6.0, 2: 4.0, 3: 9.0, 4: 3.0},
 'date': {0: '2020-12',
  1: '2021-02',
  2: '2020-12',
  3: '2020-12',
  4: '2021-01'},
 'id': {0: '12345678901',
  1: '23456789012',
  2: '34567890123',
  3: '45678901234',
  4: '56789012345'},
 'goalTrans': {0: 2.0, 1: 0.0, 2: 0.0, 3: 1.0, 4: 0.0},
 'incomes': {0: 1.0, 1: 2.0, 2: 2.0, 3: 1.0, 4: 2.0},
 'payments': {0: 0.0, 1: 0.0, 2: 0.0, 3: 0.0, 4: 1.0},
 'savings': {0: 0.0, 1: 0.0, 2: 0.0, 3: 0.0, 4: 0.0}}
1 Answers

To the point:

This can be a pretty hard catch, but you've misspecified the visible attributes because you've got three traces per your four subsets, so you'll need twelve specifications per arg and not four like you seem to have in your setup.

Solution:

The complete code snippet at the end of the answer will produce the following figure where the first buttion option has been initiated:

Subset 1:

# 00820054194
   bills     date          dni  goalTrans  incomes  payments  savings
0    1.0  2020-12  00820054194        2.0      1.0       0.0      0.0

Plot 1:

enter image description here

If you compare Subset 1 with Plot 1 you can see that we're all good so far. I'll leave it up to you to verify all subsets, but here's the last one to round things off:

Subset 4:

# 04902852446
   bills     date          dni  goalTrans  incomes  payments  savings
3    9.0  2020-12  04902852446        1.0      1.0       0.0      0.0
4    3.0  2021-01  04902852446        0.0      2.0       1.0      0.0

Plot 4:

enter image description here

And to my understanding, this should be exactly what you're looking for.

The details:

In the final iteration of:

for i,dni in enumerate(sorted(df_merged.dni.unique())):

... args in args=[{"visible": args}] looks like this:

[False, False, False, False, True]

And this does not cover all options in your setup. You see, Wwat the args in args=[{"visible": args}]) does when method = 'update' is to look for, and possibly change, the visible attribute in fig.data, which in your case is a tuple with twelve elements of which the first one looks like this:

(Bar({
     'name': 'goalTrans',
     'visible': True,
     'x': array([datetime.datetime(2020, 12, 1, 0, 0)], dtype=object),
     'y': array([2.])
 }),

Why 12? Because you've got three go.Bar() traces for four subsets of your dataframe. So in order to trigger the visibility for the correct traces and correct selection options (yes, buttons) for your update menu you don't need this:

[False, False, False, False, True]

But this:

[[True, True,True,False, False, False, False, False, False, False, False, False],
 [False, False, False, True, True, True, False, False, False, False, False, False],
 [False, False, False, False, False, False, True, True, True, False, False, False],
 [False, False, False, False, False, False, False, False, False, True, True, True]]

Or rather each element of the outer list iteratively in:

for i, dni in enumerate(sorted(df_merged.dni.unique()[:])):

... like this:

button =  dict(label=dni,
               method = 'restyle',
                args = ['visible',visibility[i]]
              )

... where, as an example, visibility[2] looks like this:

[False, False, False, False, False, False, False, False, False, True, True, True]

And that's what this somewhat cryptical part of the complete code below takes care of:

frames = len(df_merged.dni.unique())
bars = len(df.columns[3:-1])
scenarios = [list(s) for s in [e==1 for e in np.eye(frames)]]
visibility = [list(np.repeat(e, bars)) for e in scenarios]

You can definitely consider moving some of those lines out of the loop since they're repeated an unnecessary number of times, but I found it a bit more readable to keep those steps close to each other. Any more questions? Don't hesitate to ask! Here's the whole thing:

Complete code:

import pandas as pd
import numpy as np
import plotly.graph_objects as go
df_merged = pd.DataFrame({'bills': {0: 1.0, 1: 6.0, 2: 4.0, 3: 9.0, 4: 3.0},
                     'date': {0: '2020-12',
                      1: '2021-02',
                      2: '2020-12',
                      3: '2020-12',
                      4: '2021-01'},
                     'dni': {0: '00820054194',
                      1: '01717705014',
                      2: '02252584041',
                      3: '04902852446',
                      4: '04902852446'},
                     'goalTrans': {0: 2.0, 1: 0.0, 2: 0.0, 3: 1.0, 4: 0.0},
                     'incomes': {0: 1.0, 1: 2.0, 2: 2.0, 3: 1.0, 4: 2.0},
                     'payments': {0: 0.0, 1: 0.0, 2: 0.0, 3: 0.0, 4: 1.0},
                     'savings': {0: 0.0, 1: 0.0, 2: 0.0, 3: 0.0, 4: 0.0}})

fig = go.Figure()
buttons = []

frames = len(df_merged.dni.unique())
for i, dni in enumerate(sorted(df_merged.dni.unique()[:])):
    df = df_merged[df_merged['dni']==dni]
    print(dni)
    print(df)
    
    args_data = []
    bars = len(df.columns[3:-1])
    for c, column in enumerate(df.columns[3:-1]):
        fig.add_trace(go.Bar(
                        name = column,
                        x = pd.to_datetime(df.date.astype('str')),
                        y = df[column], 
                        visible=False
                      ))
    
    frames = len(df_merged.dni.unique())
    bars = len(df.columns[3:-1])
    scenarios = [list(s) for s in [e==1 for e in np.eye(frames)]]
    visibility = [list(np.repeat(e, bars)) for e in scenarios]   
        
    button =  dict(label=dni,
                   method = 'restyle',
                    args = ['visible',visibility[i]]
                  )
    
    buttons.append(button)
       
fig.update_layout(
    updatemenus=[
        dict(
        type="dropdown",
        direction="down",
        buttons = buttons)
    ],
    barmode = "stack",)
f = fig.full_figure_for_development(warn=False)

fig.show()
Related