3

I have a dataset which is similar to below one. Please note that there are multiple values for a single ID.

import pandas as pd
import numpy as np
import random

df = pd.DataFrame({'DATE_TIME':pd.date_range('2022-11-01', '2022-11-05 23:00:00',freq='h'),
                   'SBP':[random.uniform(110, 160) for n in range(120)],
                   'DBP':[random.uniform(60, 100) for n in range(120)],
                   'ID':[random.randrange(1, 100) for n in range(120)],
                   'TIMEINTERVAL':[random.randrange(1, 200) for n in range(120)]})

df['VISIT'] = df['DATE_TIME'].dt.day

df['MODE'] = np.select([df['VISIT']==1, df['VISIT'].isin([2,3])], ['New', 'InProgress'], 'Done')

I use the following DASH code to make slider:

app = Dash(__name__)


app.layout = html.Div([
    html.H4('Interactive Scatter Plot with ABPM dataset'),
    dcc.Graph(id="scatter-plot"),
    html.P("Filter by time interval:"),
    dcc.Dropdown(df.ID.unique(), id='pandas-dropdown-1'), # for choosing ID,
    dcc.RangeSlider(
        id='range-slider',
        min=0, max=600, step=10,
        marks={0: '0', 50: '50', 100: '100', 150: '150', 200: '200', 250: '250', 300: '300', 350: '350', 400: '400', 450: '450', 500: '500', 550: '550', 600: '600'},
        value=[0, 600]
    ),
    html.Div(id='dd-output-container')
])


@app.callback(
    Output("scatter-plot", "figure"),
    Input("pandas-dropdown-1", "value"),
    Input("range-slider", "value"),
    prevent_initial_call=True)

def update_bar_chart(value,slider_range):
    low, high = slider_range
    df1 = df.query("ID == @value & TIMEINTERVAL > @low & TIMEINTERVAL < @high").copy() 
    
    if df1.shape[0] != 0:
        fig = px.scatter(df1, x="DATE_TIME", y=["SBP","DBP"],
                         hover_data=['TIMEINTERVAL'],facet_col='VISIT',
                         facet_col_wrap=2,
                         symbol='MODE')
        
        fig.update_xaxes(matches= None, showticklabels=True)

        return fig
    else: 
        return dash.no_update


app.run_server(debug=True, use_reloader=False)

If df1 Visit column has value more than 1, then I would like to annotate subplots with arrow to articulate reading. To do so, I wrote the followings cript in update_bar_charts function, but it did not compile.

def update_bar_chart(value,slider_range):
    low, high = slider_range
    df1 = df.query("ID == @value & TIMEINTERVAL > @low & TIMEINTERVAL < @high").copy() 
    
    if df1.shape[0] != 0:
        fig = px.scatter(df1, x="DATE_TIME", y=["SBP","DBP"],
                         hover_data=['TIMEINTERVAL'],facet_col='VISIT',
                         facet_col_wrap=2,
                         symbol='MODE')
        
        fig.update_xaxes(matches= None, showticklabels=True)

                if df1.VISIT!=1:
                fig.add_annotation(
                xref="x domain",
                yref="y domain",
                # The arrow head will be 25% along the x axis, starting from the left
                x=0.25,
                # The arrow head will be 40% along the y axis, starting from the bottom
                y=0.4,
                arrowhead=2,
            )

                   return fig
    else: 
        return dash.no_update


app.run_server(debug=True, use_reloader=False)

What I have isenter image description here:

Whan I want to achieve is:

enter image description here

How can I add those arrows to make reading the plots easier? Number of arrows should change dynamically because each ID has different number of visits.

3
  • 1
    Firstly, "SBP","DBP" are not columns in df based on the given data. Commented Nov 16, 2022 at 17:04
  • 1
    Why do you think the best way is to use arrows? I think there is a better way to display the correct order. Commented Nov 16, 2022 at 17:55
  • @Hamzah I edited the data creation part. Thanks for the heads up. Besides, what would be the alternative way to display the coorect order? I appreciate any hints. Commented Nov 17, 2022 at 10:24

2 Answers 2

1
+100

Add annotations looping through rows of subplots. Use a 'x/y* domain' value of the 'xref'/'yref' property of an annotation, to specify a coordinate as a ratio to the x/y domain(the width/height of the frame of a subplot). Also use the 'ax'/'ay' property to specify the starting point of an arrow.

This is an example.

n_plots = len(df1['VISIT'].unique())
n_rows = (n_plots+1)//2
row_spacing = 1/((1/0.5+1) * n_rows - 1) # 50% of y domain
col_spacing = 0.1
col_spacing_in_x_domain = 1/((1/col_spacing-1)/2)
row_spacing_in_y_domain = 1/((1/row_spacing+1)/n_rows - 1)

fig = px.scatter(df1,
    facet_col='VISIT',
    facet_col_wrap=2,
    facet_row_spacing=row_spacing, facet_col_spacing=col_spacing,
    ...
)
fig.update_xaxes(matches= None, showticklabels=True)

for i in range(n_rows):
    # A row number 1 is the bottom one.
    trace = next(fig.select_traces(row=n_rows-i, col=1))
    xref, yref = trace.xaxis + ' domain', trace.yaxis + ' domain'
    if i*2+1 < n_plots:
        fig.add_annotation(
            xref=xref, yref=yref, axref=xref, ayref=yref,
            ax=1, ay=0.5,
            x=1 + col_spacing_in_x_domain, y=0.5,
            arrowhead = 2,
        )
    if i*2+2 < n_plots:
        fig.add_annotation(
            xref=xref, yref=yref, axref=xref, ayref=yref,
            ax=1 + col_spacing_in_x_domain, ay=0.5,
            x=1, y=-row_spacing_in_y_domain - 0.5,
            arrowhead = 2,
        )
Sign up to request clarification or add additional context in comments.

2 Comments

Your answer works like a charm for most of the cases. However, when the VISIT value is 2, there is no arrow plotted. Besides, when the VISIT value is 4 (or 6), there is no arrow plotted from 3 to 4 (or 5 to 6). I guess it has to do with n_plots formula. Let me know if you have hints for that.
That can be easily fixed by checking the index for each arrow. I'll update the answer.
1

I'm happy to see my answer to your other question helped. You might have to play with the arrow placements depending on the final size of your figure, but this accomplishes what you're looking for.

fig = px.line(df,
          x='DATE_TIME',
          y=['SBP', 'DBP'],
          facet_col='VISIT',
          facet_col_wrap=2,
          facet_col_spacing=0.1,
          symbol='MODE',
          markers=True)
fig.update_xaxes(matches=None,
                 showticklabels=True)

fig.add_annotation(xref='paper', yref='paper', x=0.46, y=0.1, ax=140, ay=-290, arrowhead=5, arrowsize=1, arrowwidth=3, arrowcolor='black')

fig.add_annotation(xref='paper', yref='paper', x=0.46, y=0.5, ax=140, ay=-290, arrowhead=5, arrowsize=1, arrowwidth=3, arrowcolor='black')

fig.add_annotation(xref='paper', yref='paper', x=0.54, y=0.5, ax=-140, ay=0, arrowhead=5, arrowsize=1, arrowwidth=3, arrowcolor='black')

fig.add_annotation(xref='paper', yref='paper', x=0.54, y=0.9, ax=-140, ay=0, arrowhead=5, arrowsize=1, arrowwidth=3, arrowcolor='black')

fig.show()

enter image description here

1 Comment

Thanks. But, your answer works when number of visit is 5. In other cases, it does not work properly. Approach of relent95 is more dynamic.

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.