Shared X-axis with Sub and Side-by-Side Plots and Click Events in HTML Output File

Hello,

I’m working to create a figure with multiple scatter subplots (~16-20) which are organized in multiple columns and rows. Each subplot has the same x-axis range, but different y-axes. I haven’t been able to find it, but I’m wondering if it’s possible to link all subplot x-axes such that when I zoom on one, the other plots will update? Below is an example of a truncated chart with multiple zoom levels in each chart. It would be great if they could have the same zoom level.

An alternative solution I’ve been looking into is using click events in the charts to get x-coordinates and then use fig.update_xaxes(), but the charts will be interacted with as html files and I haven’t found how to add click events to such files.

Below is the code I’m using to generate the current charts

import math
import pandas as pd
import plotly.graph_objects as go
import chart_definitions as cd
from plotly.subplots import make_subplots

#Get telemetry root folder
TELEMETRY_PATH = 'telemetry'

#Define number of subplot columns
MAX_COLUMNS = 4

#Instantiate holder for scatter objects
chart_holder = []

#Get map of chart ID's and names
charts = cd.chart_map()

#Display to user which plots are available
print('Chart ID: Chart Name')
for chart_id in charts:
    print(f'{chart_id}: {charts[chart_id]}')
    
#Query user for which charts to build
chart_selection = input('Please select from the above charts (using chart ID) the charts you would like to view. Please use csv format. ').split(',')
chart_selection = [int(n) for n in chart_selection]

#Get individual chart dictionaries which include csv locations for each metric
chart_dicts = cd.metric_selector(chart_selection)

#Determine number of rows from number of charts
rows = math.ceil(len(chart_dicts)/MAX_COLUMNS)

#Instantiate figure object
fig = make_subplots(rows=rows, cols=len(chart_dicts), shared_xaxes=True)

#Build charts
for i in range(len(chart_dicts)):
    #Get row id
    row = math.floor(i/MAX_COLUMNS) + 1
    
    #Build xaxis list
    xaxis = ['x']
    xaxis.extend([f'x{str(i)}' for i in range(1, len(chart_dicts))])
    
    #Actually build charts
    for metric in chart_dicts[i]['metrics']:
        path = f'{TELEMETRY_PATH}/' + chart_dicts[i]['metrics'][metric]['filename']
        print(f'Building subplot for {path}')
        df =  pd.read_csv(f'{TELEMETRY_PATH}/' + chart_dicts[i]['metrics'][metric]['filename'])[['timestamp', 'value']].sort_values(by='timestamp')
        chart_holder.append(fig.add_trace(go.Scatter(x=df['timestamp'], y=df['value'], name=metric, xaxis=xaxis[i]), row=row, col=i+1))
    
#Add hovermode and hide legend     
fig.update_layout(hovermode='x', showlegend=False)

#Write to html file for sharing
fig.write_html('Telemetry_figure.html', auto_open=True)

Hi @ngaudio welcome to the forum! You can link together the different xaxes by using the matches attribute (https://plot.ly/python/reference/#layout-xaxis-matches). See for example the code below (we should add one such example in https://plot.ly/python/axes/, https://plot.ly/python/facet-plots/ and https://plot.ly/python/subplots/)

import plotly.graph_objects as go
from plotly.subplots import make_subplots
import numpy as np

N = 100
x = np.linspace(0, 1, N)
fig = make_subplots(2, 2)
for i in range(1, 3):
    for j in range(1, 3):
        fig.append_trace(go.Scatter(x=x, y=np.random.random(N)), i, j)
fig.update_xaxes(matches='x')
fig.show()

If you are able to use a plotly express function with facets (see https://plot.ly/python/facet-plots/), then axes are already matched together by default.

2 Likes

Hi @Emmanuelle! Thank you very much for your response! I came across matches, but in my experimentation I couldn’t get it to work. Turns out I was implementing it wrong.

Anyway, your solution worked! Thank you very much!

Hi @Emmanuelle

Sorry for the necrobump. It isn’t clear to me from the examples or the documentation to what the matches parameter is referring. I see that it is a parameter you specify on the entire figure, not each individual subplot…

For example, suppose I created a figure with 2x2 subplots, and I want all of them to share x axes except for the plot in the bottom right which should move/zoom independently. How would I specify matches to get this behavior? Is it the name of the axes given by fig.data[N].xaxis? How would you specify multiple axes that should be linked?

Thanks!

@Yankee, is this what you are looking for?

df = pd.DataFrame({'x':[1,2,3,4,1,2,3,4,1,2,3,4,5,6,7,8],'y':[1,2,3,4,1,2,3,4,1,2,3,4,5,6,7,8], 
                  'c':['a','a','a','a','b','b','b','b','c','c','c','c','d','d','d','d']})

fig = px.scatter(df, x="x", y="y", facet_col="c", facet_col_wrap=2,)
fig.update_xaxes(showticklabels=True)
fig.update_yaxes(showticklabels=True)
fig.update_xaxes(matches=None,row = 1,col=2)
fig.show()

1 Like

@alooksha Wow, yeah. That has exactly the behavior I described. Thank you for the example. I am going to try to recreate that without using plotly express.

df = pd.DataFrame({'x':[1,2,3,4,1,2,3,4,1,2,3,4,5,6,7,8],'y':[1,2,3,4,1,2,3,4,1,2,3,4,5,6,7,8], 
                  'c':['a','a','a','a','b','b','b','b','c','c','c','c','d','d','d','d']})

fig = go.FigureWidget(make_subplots(rows=2, cols=2, shared_xaxes=False))
fig.add_scatter(x=df['x'], y=df['y'], row=1, col=1)
fig.add_scatter(x=df['x'], y=df['y'], row=1, col=2)
fig.add_scatter(x=df['x'], y=df['y'], row=2, col=1)
fig.add_scatter(x=df['x'], y=df['y'], row=2, col=2)
fig.update_xaxes(row=1, col=1, matches='x5')
fig.update_xaxes(row=1, col=2, matches='x5')
fig.update_xaxes(row=2, col=1, matches='x5')

I think I understand how it works now. I did not understand that the update_xaxes command allowed you to select a single subplot at a time. I guess you have to assign an unused axis id that all of them will share, e.g. x5 as I have done.

Thank you so much!

@Yankee, you need to subset the dataframe for each trace. Updated code below.

df = pd.DataFrame({'x':[1,2,3,4,1,2,3,4,1,2,3,4,5,6,7,8],'y':[1,2,3,4,1,2,3,4,1,2,3,4,5,6,7,8], 
                  'c':['a','a','a','a','b','b','b','b','c','c','c','c','d','d','d','d']})

fig = go.FigureWidget(make_subplots(rows=2, cols=2, shared_xaxes=False))
fig.add_scatter(x=df[df['c'] == 'a']['x'], y=df[df['c'] == 'a']['y'], row=1, col=1)
fig.add_scatter(x=df[df['c'] == 'b']['x'], y=df[df['c'] == 'b']['y'], row=1, col=2)
fig.add_scatter(x=df[df['c'] == 'c']['x'], y=df[df['c'] == 'c']['y'], row=2, col=1)
fig.add_scatter(x=df[df['c'] == 'd']['x'], y=df[df['c'] == 'd']['y'], row=2, col=2)
fig.update_xaxes(row=1, col=1, matches='x')
fig.update_xaxes(row=1, col=2, matches='x')
fig.update_xaxes(row=2, col=1, matches='x')
fig.show()

If you see Layout object:

Layout({
    'template': '...',
    'xaxis': {'anchor': 'y', 'domain': [0.0, 0.45], 'matches': 'x'},
    'xaxis2': {'anchor': 'y2', 'domain': [0.55, 1.0], 'matches': 'x'},
    'xaxis3': {'anchor': 'y3', 'domain': [0.0, 0.45], 'matches': 'x'},
    'xaxis4': {'anchor': 'y4', 'domain': [0.55, 1.0]},
    'yaxis': {'anchor': 'x', 'domain': [0.575, 1.0]},
    'yaxis2': {'anchor': 'x2', 'domain': [0.575, 1.0]},
    'yaxis3': {'anchor': 'x3', 'domain': [0.0, 0.425]},
    'yaxis4': {'anchor': 'x4', 'domain': [0.0, 0.425]}
})

Hope this helps!

1 Like