1

I'm trying to create an interactive PCA plot using plotly-express and graph objects in python (go.Scatter).

The plot should have 2 dropdowns menus (for x-axis and y-axis) to change between the first 5 PCA in the data.

Each data point also belongs to a treatment group either Before, After, or QC.

I was able to plot the PCA1 and PCA2 with plotly-express package but when trying to add the 2 dropdown menus that will update the plot between the 5 PCA it become a mess.

The example data is in my GitHub link,the first 5 columns are the first 5 PCAs.

The code the generate PC1 vs PC2 is:


labels={'0': 'PC 1 (22.0%)',
 '1': 'PC 2 (19.6%)',
 '2': 'PC 3 (11.1%)',
 '3': 'PC 4 (8.2%)',
 '4': 'PC 5 (3.9%)',
 'color': 'Group'}

fig1 = px.scatter(components_df, x=0 , y=2 ,
                  color = 'Class',
                  width=1000, height=700,
                 template='presentation',
                 labels=labels, 
                 title="PCA Score Plot (PC{} vs. PC{})".format(1, 2) ,
                 hover_data=['idx', 'SampleID']
                )
fig1.show()

and it looks like this : enter image description here

I'm trying to add 2 dropdown menus like I draw above to update the x-axis and the y-axis with the different PC's.

So first step was to add_trace on the figure to add other PCs to the figure but dont know how to add graph object to plotly-express to that what i did:

fig = go.Figure()
for Class, group in components_df.groupby("Class"):
#     print(group[0])
    
    fig.add_trace(go.Scatter(x=group[0], y=group[1], name=Class, mode='markers', 
      hovertemplate="Class=%s<br>PC1=%%{x}<br>PC2=%%{y}<extra></extra>"% Class))    

for Class, group in components_df.groupby("Class"):
#     print(group[0])
    
    fig.add_trace(go.Scatter(x=group[0], y=group[2], name=Class, mode='markers', 
      hovertemplate="Class=%s<br>PC1=%%{x}<br>PC3=%%{y}<extra></extra>"% Class))  

fig.update_layout(
    updatemenus=[go.layout.Updatemenu(
        active=0,
        buttons=list(
            [dict(label = 'All',
                  method = 'update',
                  args = [{'visible': [True, True, True, True,True]},
                          {'title': 'All',
                           'showlegend':True}]),
             dict(label = 'PC1 PC1',
                  method = 'update',
                  args = [{'visible': [True, False, False, False, False]}, # the index of True aligns with the indices of plot traces
                          {'title': 'PC1 PC1',
                           'showlegend':True}]),
             dict(label = 'PC1 PC2',
                  method = 'update',
                  args = [{'visible': [False, True, False, False, False]},
                          {'title': 'AAPL',
                           'showlegend':True}]),
             dict(label = 'PC1 PC3',
                  method = 'update',
                  args = [{'visible': [False, False, True, False, False]},
                          {'title': 'AMZN',
                           'showlegend':True}]),
            ])
        )
    ])

and that is the result:

enter image description here

There are many problems with that:

  1. when changing the different options in the dropdown menu also the legends change (they suppose the stay fixed)
  2. when changing the different options in the dropdown menu it does not lools like the data should be
  3. it does not look nice like in the plotly-express.
  4. there is only one dropdown

The code is base on many explanations in the documentation and blogs:

  1. How to change plot data using dropdowns
  2. Dropdown Menus in Python
  3. Adding interactive filters
  4. Setting the Font, Title, Legend Entries, and Axis Titles in Python

Any hint will be appreciated on how to add correct add_trac or correct dropdown menu

Thank you!!!

1 Answer 1

1
  • it's all about being highly structured and systematic. Plotly Express does generate a decent base chart. Use fig1.to_dict() to view graph object structures it has built
  • challenge I found with adding updatemenus to Plotly Express figure - it's a multi-trace figure with trace defining marker color. This can be simplified to a single trace figure with an array defining marker color
  • then it's a case of building updatemenus. This I have done as nested list comprehensions. Outer loop axis (each menu), inner loop principle component (each menu item)

Updates

  • magic colors - fair critique. I had used a hard coded dict for color mapping. Now programmatically build cmap Reverted back to static definition of cmap as dict comprehension is not wanted. Changed to a pandas approach to building cmap with lambda function
  • "y": 1 if ax == "x" else 0.9 We are building two drop downs, one for xaxis and one for yaxis. Hopefully it's obvious that the positions of these menus needs to be different. See docs: https://plotly.com/python/reference/layout/updatemenus/ For similar reason active property s being set. Make sure drop downs show what is actually plotted in the figure
  • legend refer back to point I made about multi-trace figures. Increases complexity! Have to use synthetic traces and this technique Plotly: How to update one specific trace using updatemenus?
import pandas as pd
import plotly.graph_objects as go
import plotly.express as px

components_df = pd.read_csv(
    "https://raw.githubusercontent.com/TalWac/stakoverflow-Qustion/main/components_df.csv"
)

labels = {
    "0": "PC 1 (22.0%)",
    "1": "PC 2 (19.6%)",
    "2": "PC 3 (11.1%)",
    "3": "PC 4 (8.2%)",
    "4": "PC 5 (3.9%)",
    "color": "Group",
}

# cmap = {
#     cl: px.colors.qualitative.Plotly[i]
#     for i, cl in enumerate(
#         components_df.groupby("Class", as_index=False).first()["Class"]
#     )
# }
# revert back to static dictionary as dynamic building is not wanted
# cmap = {'After': '#636EFA', 'Before': '#EF553B', 'QC': '#00CC96'}
# use lambda functions instead of dict comprehension
df_c = components_df.groupby("Class", as_index=False).first()
df_c["color"] = df_c.apply(lambda r: px.colors.qualitative.Plotly[r.name], axis=1)
cmap = df_c.set_index("Class").loc[:,"color"].to_dict()

fig1 = go.Figure(
    go.Scatter(
        x=components_df["0"],
        y=components_df["1"],
        customdata=components_df.loc[:, ["idx", "SampleID", "Class"]],
        marker_color=components_df["Class"].map(cmap),
        mode="markers",
        hovertemplate="Class=%{customdata[2]}<br>x=%{x}<br>y=%{y}<br>idx=%{customdata[0]}<br>SampleID=%{customdata[1]}<extra></extra>",
    )
).update_layout(
    template="presentation",
    xaxis_title_text=labels["0"],
    yaxis_title_text=labels["1"],
    height=700,
)

fig1.update_layout(
    updatemenus=[
        {
            "active": 0 if ax == "x" else 1,
            "buttons": [
                {
                    "label": f"{ax}-PCA{pca+1}",
                    "method": "update",
                    "args": [
                        {ax: [components_df[str(pca)]]},
                        {f"{ax}axis": {"title": {"text": labels[str(pca)]}}},
                        [0],
                    ],
                }
                for pca in range(5)
            ],
            "y": 1 if ax == "x" else 0.9,
        }
        for ax in ["x", "y"]
    ]
).update_traces(showlegend=False)

# add a legend by using synthetic traces.  NB, this will leave markers at 0,0
fig1.add_traces(
    px.scatter(
        components_df.groupby("Class", as_index=False).first(),
        x="0",
        y="1",
        color="Class",
        color_discrete_map=cmap,
    )
    .update_traces(x=[0], y=[0])
    .data
)

enter image description here

Sign up to request clarification or add additional context in comments.

7 Comments

-Thank you very much for your time, it looks really amazing! with your permission I would like to ask 1). if it is possible to add a legend explaining which color represents each group like in my first plot upper-right side Class. 2). how did you know how to select the colors #1F77B4, #FF7F0E, #2CA02C to fit the template="presentation" colors, in cases I have more than 3 groups (Before, After, and QC). 3). in the code the line :"y": 1 if ax == "x" else 0.9, what it does, I tried to change the 1 and the 0.9 to different numbers and it changed the location of the dropdown menu. Many thanks!
@ Rob Raymond - Thank you very much for the elaborate explantion! In the creation of cmap dictionary there is a for loop with enumerating but above it I did not understand what is the object in the first row- cl: px.colors.qualitative.Plotly[i]. it's not a lambda expression, nor list comprehension, right?
@ Rob Raymond- the dict comprehension (which is new to me) and the lambda expression are great solutions for me, Thank you! I'd like to ask you what complexity is the legend adds? and a second question about lambda expression, the object - [r.name]. r is when you run through the color in px.colors.qualitative.Plotly, right? but what is name? I could not figure this out. And must say I have been learning a lot from your coding, Thank you very much
@TaL have you had the opportunity to define the none functional constraints of this solution? I'd like to complete this answer or remove it if it cannot meet all constraints you need to apply to solution
I'm not familiar with the term>"none functional constraints of this solution" so I do not know how to answer the question. The solution you provided is very good for me, and I appreciate your help, Thank you very much
|

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.