Search code examples
pythonscatter-plotplotly-python

How can I add annotations outside a Plotly "Quandrant" Chart?


I'm working on a quadrant chart, but a bit stumped on how to get some annotations outside of my Plotly quadrant chart. I have provided some data data and code that I used below.

#the libraries
import pandas as pd
import numpy as np

import matplotlib.pyplot as plt
import plotly.express as px
import plotly.graph_objects as go
import seaborn as sns

Sample of the data that I used:

#the data
data = pd.DataFrame({
    'label': ['bf', 'lv', 'emp', 'ovg', 'cfr', 'osc', 'osh', 'osc', 'ost', 'osp'],
    'score': [0.407, 0.392, 0.562, 0.456, 0.350, 0.410, 0.362, 0.373, 0.313, 0.342],
    'wgt': [26.43, 16.80, 16.08, 15.86, 6.83, 5.31, 4.77, 2.99, 2.80, 2.13]
    })
print(data)

Finally, the code that I used in generating the quadrant chart is below. Some of it borrowed from this Medium article.

fig = px.scatter(data, x=data.score, y=data.wgt, text=data.label, 
                 title='main_title',
                 width=800, height=600)

# calculate averages
x_avg = data['score'].mean()
y_avg = data['wgt'].mean()

# add horizontal and vertical lines
fig.add_vline(x=x_avg, line_width=1, opacity=0.2)
fig.add_hline(y=y_avg, line_width=1, opacity=0.2)

# set x limits
adj_x = max((data['score'].max() - x_avg), (x_avg - data['score'].min())) * 1.1
lb_x, ub_x = (x_avg - adj_x, x_avg + adj_x)
fig.update_xaxes(range = [lb_x, ub_x])

# set y limits
adj_y = max((data['wgt'].max() - y_avg), (y_avg - data['wgt'].min())) * 1.1
lb_y, ub_y = (y_avg - adj_y, y_avg + adj_y)
fig.update_yaxes(range = [lb_y, ub_y])

# update x tick labels
axis = ['Low', 'High']     
fig.update_layout(
    xaxis_title='xtitle',
    xaxis = dict(
        tickmode = 'array',
        tickvals = ([(x_avg - adj_x / 2), (x_avg + adj_x / 2)]),
        ticktext = axis
      )
    )

# update y tick labels
fig.update_layout(
    yaxis_title='ytitle',
    yaxis = dict(
        tickmode = 'array',
        tickvals = ([(y_avg - adj_y / 2), (y_avg + adj_y / 2)]),
        ticktext = axis,
        tickangle=270
        )
    ) 

fig.update_layout(margin=dict(t=50, l=5, r=5, b=50),
    title={'text': 'pl',
           'font_size': 20,
           'y':1.0,
           'x':0.5,
           'xanchor': 'center',
           'yanchor': 'top'})

# where I need the help with annotation
fig.add_annotation(x=data['score'].min(), y=data['wgt'].min(),
                   text="Quad 3",
                   yref='paper', 
                   showarrow=False)
fig.add_annotation(x=data['score'].max(), y=data['wgt'].min(),
                   text="Quad 4",
                   yref='paper',
                   showarrow=False)
fig.add_annotation(x=data['score'].min(), y=data['wgt'].max(),
                   text="Quad 1",
                   yref='paper',
                   showarrow=False)
fig.add_annotation(x=data['score'].max(), y=data['wgt'].max(),
                   text="Quad 2",
                   yref='paper',
                   showarrow=False)
fig.show()

The final output I am looking for should look like the chart below. So, specifically, what I need some assistance with is how to position the texts Quad 1 through 4 outside as shown below : enter image description here


Solution

  • You need to add margins and then set the coordinates accordingly for each position. here is a code I used with absolute coordinates.

    fig = px.scatter(data, x=data.score, y=data.wgt, text=data.label, 
                     title='main_title',
                     width=800, height=600)
    
    # calculate averages
    x_avg = data['score'].mean()
    y_avg = data['wgt'].mean()
    
    # add horizontal and vertical lines
    fig.add_vline(x=x_avg, line_width=1, opacity=0.2)
    fig.add_hline(y=y_avg, line_width=1, opacity=0.2)
    
    # set x limits
    adj_x = max((data['score'].max() - x_avg), (x_avg - data['score'].min())) * 1.1
    lb_x, ub_x = (x_avg - adj_x, x_avg + adj_x)
    fig.update_xaxes(range = [lb_x, ub_x])
    
    # set y limits
    adj_y = max((data['wgt'].max() - y_avg), (y_avg - data['wgt'].min())) * 1.1
    lb_y, ub_y = (y_avg - adj_y, y_avg + adj_y)
    fig.update_yaxes(range = [lb_y, ub_y])
    
    # update x tick labels
    axis = ['Low', 'High']     
    fig.update_layout(
        xaxis_title='xtitle',
        xaxis = dict(
            tickmode = 'array',
            tickvals = ([(x_avg - adj_x / 2), (x_avg + adj_x / 2)]),
            ticktext = axis
          )
        )
    
    # update y tick labels
    fig.update_layout(
        yaxis_title='ytitle',
        yaxis = dict(
            tickmode = 'array',
            tickvals = ([(y_avg - adj_y / 2), (y_avg + adj_y / 2)]),
            ticktext = axis,
            tickangle=270
            )
        ) 
    
    fig.update_layout(margin=dict(t=50, l=5, r=5, b=50),
        title={'text': 'pl',
               'font_size': 20,
               'y':1.0,
               'x':0.5,
               'xanchor': 'center',
               'yanchor': 'top'})
    
    # where I need the help with annotation
    fig.add_annotation(dict(font=dict(color="black",size=18),
                            x=0, y=-0.15,#data['score'].min()-0.2, y=data['wgt'].min()-0.2,
                            text="Quad 3",
                            xref='paper',
                            yref='paper', 
                            showarrow=False))
    fig.add_annotation(dict(font=dict(color="black",size=18),
                            x=1, y=-0.15,#x=data['score'].max(), y=data['wgt'].min(),
                            text="Quad 4",
                            xref='paper',
                            yref='paper',
                            showarrow=False))
    fig.add_annotation(dict(font=dict(color="black",size=18),
                            x=0, y=1.15, #x=data['score'].min(), y=data['wgt'].max(),
                            text="Quad 1",
                            xref='paper',
                            yref='paper',
                            showarrow=False))
    fig.add_annotation(dict(font=dict(color="black",size=18),
                            x=1, y=1.15, #x=data['score'].max(), y=data['wgt'].max(),
                            text="Quad 2",
                            xref='paper',
                            yref='paper',
                            showarrow=False))
    
    fig.update_layout(
        margin=dict(l=20, r=20, t=100, b=100),
    )
    
    
    fig.show()
    
    
    

    updated plot