plotly / plotly.js

Open-source JavaScript charting library behind Plotly and Dash
https://plotly.com/javascript/
MIT License
16.9k stars 1.85k forks source link

add Y-axis autoscaling with X-axis range change #6995

Open nzjrs opened 4 months ago

nzjrs commented 4 months ago

This is a common request.

I would expect when I zoom on the x-axis, in a linked plot configuration, the y-axis would scale to the selected/zoomed range.

Here are reports of users requesting this feature

Emucde commented 1 week ago

I've also spent a lot of time looking into whether and how it's possible in Plotly to automatically update the y-axis after zooming in on only the x-axis. I'm actually really enthusiastic about Plotly, so I was very surprised to see how such a feature, which is present in virtually every serious plotting program, could be missing. After extensive research, I found out that there's simply no direct way to do this. Communication with the developers was generally always something like this:

User: Feature Request: Please implement that when zooming in on x, y is also automatically updated. Developer: We're already working on it, it should be ready in 2, 3... User: Weeks? Months? Years? Developer: ... Or even 4.

All joking aside, I would like to express my greatest respect to the developers who make such a modern tool freely available.

To the solution: We are definitely not the first users and we won't be the last to have dealt with this.

The approach is based on the idea of supplementing the y-autoscale feature directly in JavaScript. There were some suggestions from

as well as useful links to documentation mentioned therein

Enclosed is a code suggestion that should at least make the idea clear:

Code suggestion
Prepare data for Plotly ```Python import numpy as np from scipy.spatial.transform import Rotation import scipy as sp from scipy.signal import savgol_filter import time import crocoddyl import pinocchio import matplotlib.pyplot as plt import meshcat.geometry as g import meshcat.transformations as tf import webbrowser import os from meshcat.animation import Animation from pinocchio.visualize import MeshcatVisualizer import plotly.graph_objects as go from plotly.subplots import make_subplots import plotly.offline as py from typing import overload from bs4 import BeautifulSoup def calc_7dof_data(us, xs, t, TCP_frame_id, robot_model, param_trajectory, frep_per_Ta_step): robot_data = robot_model.createData() N = len(xs) n = robot_model.nq p_e = np.zeros((N, 3)) p_e_p = np.zeros((N, 3)) p_e_pp = np.zeros((N, 3)) quat_e = np.zeros((N, 4)) omega_e = np.zeros((N, 3)) omega_e_p = np.zeros((N, 3)) p_d = param_trajectory['p_d'] p_d_p = param_trajectory['p_d_p'] p_d_pp = param_trajectory['p_d_pp'] quat_d = param_trajectory['q_d'] omega_d = param_trajectory['omega_d'] omega_d_p = param_trajectory['omega_d_p'] w = np.zeros(N) # manipulability q = np.zeros((N, n)) q_p = np.zeros((N, n)) q_pp = np.zeros((N, n)) p_err = np.zeros((N, 3)) p_err_p = np.zeros((N, 3)) p_err_pp = np.zeros((N, 3)) e_x = np.zeros(N) e_x_p = np.zeros(N) e_x_pp = np.zeros(N) e_y = np.zeros(N) e_y_p = np.zeros(N) e_y_pp = np.zeros(N) e_z = np.zeros(N) e_z_p = np.zeros(N) e_z_pp = np.zeros(N) quat_err = np.zeros((N, 4)) omega_err = np.zeros((N, 3)) omega_err_p = np.zeros((N, 3)) tau = np.zeros((N, n)) window_length = 301 poly_order = 2 frep_per_Ta_step_mean = smooth_signal_savgol(frep_per_Ta_step, window_length, poly_order) for i in range(N): q[i] = xs[i, 0:n] q_p[i] = xs[i, n:2 * n] tau[i] = us[i] q_pp[i] = pinocchio.aba(robot_model, robot_data, q[i], q_p[i], tau[i]) pinocchio.forwardKinematics(robot_model, robot_data, q[i], q_p[i], q_pp[i]) pinocchio.updateFramePlacements(robot_model, robot_data) pinocchio.computeJointJacobians(robot_model, robot_data, q[i]) J = pinocchio.getFrameJacobian(robot_model, robot_data, TCP_frame_id, pinocchio.ReferenceFrame.LOCAL_WORLD_ALIGNED) pinocchio.computeJointJacobiansTimeVariation(robot_model, robot_data, q[i], q_p[i]) J_p = pinocchio.getFrameJacobianTimeVariation(robot_model, robot_data, TCP_frame_id, pinocchio.ReferenceFrame.LOCAL_WORLD_ALIGNED) p_e[i] = robot_data.oMf[TCP_frame_id].translation.T.copy() p_e_p[i] = J[0:3, :] @ q_p[i] # p_e_pp[i] = J[0:3, :] @ q_pp[i] + J_p[0:3, :] @ q_p[i] p_e_pp[i] = pinocchio.getFrameClassicalAcceleration(robot_model, robot_data, TCP_frame_id, pinocchio.ReferenceFrame.LOCAL_WORLD_ALIGNED).linear quat_e_7val = pinocchio.SE3ToXYZQUAT(robot_data.oMf[TCP_frame_id]) quat_e_xyzw = pinocchio.Quaternion(quat_e_7val[3::]) quat_e[i] = np.hstack([quat_e_7val[6], quat_e_7val[3:6]]) #wxyz omega_e[i] = pinocchio.getFrameVelocity(robot_model, robot_data, TCP_frame_id, pinocchio.ReferenceFrame.LOCAL_WORLD_ALIGNED).angular omega_e_p[i] = pinocchio.getFrameAcceleration(robot_model, robot_data, TCP_frame_id, pinocchio.ReferenceFrame.LOCAL_WORLD_ALIGNED).angular w[i] = np.sqrt(sp.linalg.det(J @ J.T)) p_err[i] = p_e[i] - p_d[:, i] p_err_p[i] = p_e_p[i] - p_d_p[:, i] p_err_pp[i] = p_e_pp[i] - p_d_pp[:, i] e_x[i] = p_err[i][0] e_y[i] = p_err[i][1] e_z[i] = p_err[i][2] e_x_p[i] = p_err_p[i][0] e_y_p[i] = p_err_p[i][1] e_z_p[i] = p_err_p[i][2] e_x_pp[i] = p_err_pp[i][0] e_y_pp[i] = p_err_pp[i][1] e_z_pp[i] = p_err_pp[i][2] quat_d_xyzw = pinocchio.Quaternion( np.hstack([quat_d[1:, i], quat_d[0, i]]) ) q_d_xyzw_inv = pinocchio.Quaternion.inverse(quat_d_xyzw) quat_err_temp = quat_e_xyzw * q_d_xyzw_inv quat_err[i] = np.array([quat_err_temp.w, quat_err_temp.x, quat_err_temp.y, quat_err_temp.z]) omega_err[i] = omega_e[i] - param_trajectory['omega_d'][:, i] omega_err_p[i] = omega_e_p[i] - param_trajectory['omega_d_p'][:, i] # make data shorter, use only each N_dec sample N_dec = 100 t = t[::N_dec] p_e = p_e[::N_dec] p_e_p = p_e_p[::N_dec] p_e_pp = p_e_pp[::N_dec] quat_e = quat_e[::N_dec] omega_e = omega_e[::N_dec] omega_e_p = omega_e_p[::N_dec] p_d = p_d[:, ::N_dec] p_d_p = p_d_p[:, ::N_dec] p_d_pp = p_d_pp[:, ::N_dec] quat_d = quat_d[:, ::N_dec] omega_d = omega_d[:, ::N_dec] omega_d_p = omega_d_p[:, ::N_dec] p_err = p_err[::N_dec] p_err_p = p_err_p[::N_dec] p_err_pp = p_err_pp[::N_dec] e_x = e_x[::N_dec] e_y = e_y[::N_dec] e_z = e_z[::N_dec] e_x_p = e_x_p[::N_dec] e_y_p = e_y_p[::N_dec] e_z_p = e_z_p[::N_dec] e_x_pp = e_x_pp[::N_dec] e_y_pp = e_y_pp[::N_dec] e_z_pp = e_z_pp[::N_dec] quat_err = quat_err[::N_dec] omega_err = omega_err[::N_dec] omega_err_p = omega_err_p[::N_dec] w = w[::N_dec] q = q[::N_dec] q_p = q_p[::N_dec] q_pp = q_pp[::N_dec] tau = tau[::N_dec] # create subplot data array subplot1 = {'title': 'p_e (m) (Y: x, B: y, P: z)', 'sig_labels': ['p_e_x (m)', 'p_e_y (m)', 'p_e_z (m)', 'p_d_x (m)', 'p_d_y (m)', 'p_d_z (m)'], 'sig_xdata': t, 'sig_ydata': [p_e[:, 0], p_e[:, 1], p_e[:, 2], p_d[0], p_d[1], p_d[2]], 'sig_linestyles': ['-', '-', '-', '--', '--', '--'], 'sig_colors': ['rgb(255,255,17)', 'rgb(19,159,255)', 'rgb(255,153,200)', 'rgb(0,0,255)', 'rgb(255,0,0)', 'rgb(0,127,0)']} subplot2 = {'title': 'ṗ_e (m/s) (Y: x, B: y, P: z)', 'sig_labels': ['ṗ_e_x (m/s)', 'ṗ_e_y (m/s)', 'ṗ_e_z (m/s)', 'ṗ_d_x (m/s)', 'ṗ_d_y (m/s)', 'ṗ_d_z (m/s)'], 'sig_xdata': t, 'sig_ydata': [p_e_p[:, 0], p_e_p[:, 1], p_e_p[:, 2], p_d_p[0], p_d_p[1], p_d_p[2], ], 'sig_linestyles': ['-', '-', '-', '--', '--', '--'], 'sig_colors': ['rgb(255,255,17)', 'rgb(19,159,255)', 'rgb(255,153,200)', 'rgb(0,0,255)', 'rgb(255,0,0)', 'rgb(0,127,0)']} subplot3 = {'title': 'p̈_e (m/s²) (Y: x, B: y, P: z)', 'sig_labels': ['p̈_e_x (m/s²)', 'p̈_e_y (m/s²)', 'p̈_e_z (m/s²)', 'p̈_d_x (m/s²)', 'p̈_d_y (m/s²)', 'p̈_d_z (m/s²)'], 'sig_xdata': t, 'sig_ydata': [p_e_pp[:, 0], p_e_pp[:, 1], p_e_pp[:, 2], p_d_pp[0], p_d_pp[1], p_d_pp[2]], 'sig_linestyles': ['-', '-', '-', '--', '--', '--'], 'sig_colors': ['rgb(255,255,17)', 'rgb(19,159,255)', 'rgb(255,153,200)', 'rgb(0,0,255)', 'rgb(255,0,0)', 'rgb(0,127,0)']} # manipulate subplot data array subplot4 = {'title': 'Manip. w = √( det( JJ⸆ ) )', 'sig_labels': ['Manip. w = √( det( JJ⸆ ) )'], 'sig_xdata': t, 'sig_ydata': [w], 'sig_linestyles': ['-'], 'sig_colors': ['rgb(255,255,17)']} subplot5 = {'title': 'e_x (m)', 'sig_labels': ['e_x (m)'], 'sig_xdata': t, 'sig_ydata': [e_x], 'sig_linestyles': ['-'], 'sig_colors': ['rgb(255,255,17)']} subplot6 = {'title': 'ė_x (m/s)', 'sig_labels': ['ė_x (m/s)'], 'sig_xdata': t, 'sig_ydata': [e_x_p], 'sig_linestyles': ['-'], 'sig_colors': ['rgb(255,255,17)']} subplot7 = {'title': 'ё_x (m/s²)', 'sig_labels': ['ё_x (m/s²)'], 'sig_xdata': t, 'sig_ydata': [e_x_pp], 'sig_linestyles': ['-'], 'sig_colors': ['rgb(255,255,17)']} subplot8 = {'title': 'q (rad)', 'sig_labels': ['q1', 'q2', 'q3', 'q4', 'q5', 'q6'], 'sig_xdata': t, 'sig_ydata': [q[:, 0], q[:, 1], q[:, 2], q[:, 3], q[:, 4], q[:, 5]], 'sig_linestyles': ['-', '-', '-', '-', '-', '-'], 'sig_colors': ['rgb(255,255,17)', 'rgb(19,159,255)', 'rgb(255,105,41)', 'rgb(100,212,19)', 'rgb(183,70,255)', 'rgb(15,255,255)']} subplot9 = {'title': 'e_y (m)', 'sig_labels': ['e_y (m)'], 'sig_xdata': t, 'sig_ydata': [e_y], 'sig_linestyles': ['-'], 'sig_colors': ['rgb(19,159,255)']} subplot10 = {'title': 'ė_y (m/s)', 'sig_labels': ['ė_y (m/s)'], 'sig_xdata': t, 'sig_ydata': [e_y_p], 'sig_linestyles': ['-'], 'sig_colors': ['rgb(19,159,255)']} subplot11 = {'title': 'ё_y (m/s²)', 'sig_labels': ['ё_y (m/s²)'], 'sig_xdata': t, 'sig_ydata': [e_y_pp], 'sig_linestyles': ['-'], 'sig_colors': ['rgb(19,159,255)']} subplot12 = {'title': 'q̇ (rad/s)', 'sig_labels': ['q̇_1', 'q̇_2', 'q̇_3', 'q̇_4', 'q̇_5', 'q̇_6'], 'sig_xdata': t, 'sig_ydata': [q_p[:, 0], q_p[:, 1], q_p[:, 2], q_p[:, 3], q_p[:, 4], q_p[:, 5]], 'sig_linestyles': ['-', '-', '-', '-', '-', '-'], 'sig_colors': ['rgb(255,255,17)', 'rgb(19,159,255)', 'rgb(255,105,41)', 'rgb(100,212,19)', 'rgb(183,70,255)', 'rgb(15,255,255)']} subplot13 = {'title': 'e_z (m)', 'sig_labels': ['e_z (m)'], 'sig_xdata': t, 'sig_ydata': [e_z], 'sig_linestyles': ['-'], 'sig_colors': ['rgb(255,153,200)']} subplot14 = {'title': 'ė_z (m/s)', 'sig_labels': ['ė_z (m/s)'], 'sig_xdata': t, 'sig_ydata': [e_z_p], 'sig_linestyles': ['-'], 'sig_colors': ['rgb(255,153,200)']} subplot15 = {'title': 'ё_z (m/s²)', 'sig_labels': ['ё_z (m/s²)'], 'sig_xdata': t, 'sig_ydata': [e_z_pp], 'sig_linestyles': ['-'], 'sig_colors': ['rgb(255,153,200)']} subplot16 = {'title': 'q̈ (rad/s²)', 'sig_labels': ['q̈_1', 'q̈_2', 'q̈_3', 'q̈_4', 'q̈_5', 'q̈_6'], 'sig_xdata': t, 'sig_ydata': [q_pp[:, 0], q_pp[:, 1], q_pp[:, 2], q_pp[:, 3], q_pp[:, 4], q_pp[:, 5]], 'sig_linestyles': ['-', '-', '-', '-', '-', '-'], 'sig_colors': ['rgb(255,255,17)', 'rgb(19,159,255)', 'rgb(255,105,41)', 'rgb(100,212,19)', 'rgb(183,70,255)', 'rgb(15,255,255)']} subplot17 = {'title': 'quat_e(2:4)', 'sig_labels': ['quat_e_2', 'quat_e_3', 'quat_e_4', 'quat_d_2', 'quat_d_3', 'quat_d_4'], 'sig_xdata': t, 'sig_ydata': [quat_e[:, 1], quat_e[:, 2], quat_e[:, 3], quat_d[1], quat_d[2], quat_d[3]], 'sig_linestyles': ['-', '-', '-', '--', '--', '--'], 'sig_colors': ['rgb(255,255,17)', 'rgb(19,159,255)', 'rgb(255,105,41)', 'rgb(0,0,255)', 'rgb(255,0,0)', 'rgb(0,127,0)']} subplot18 = {'title': 'ω_e (rad/s)', 'sig_labels': ['ω_e_x (rad/s)', 'ω_e_y (rad/s)', 'ω_e_z (rad/s)', 'ω_d_x (rad/s)', 'ω_d_y (rad/s)', 'ω_d_z (rad/s)'], 'sig_xdata': t, 'sig_ydata': [omega_e[:, 0], omega_e[:, 1], omega_e[:, 2], omega_d[0], omega_d[1], omega_d[2]], 'sig_linestyles': ['-', '-', '-', '--', '--', '--'], 'sig_colors': ['rgb(255,255,17)', 'rgb(19,159,255)', 'rgb(255,105,41)', 'rgb(0,0,255)', 'rgb(255,0,0)', 'rgb(0,127,0)']} subplot19 = {'title': 'ὠ_e (rad/s²)', 'sig_labels': ['ὠ_e_x (rad/s²)', 'ὠ_e_y (rad/s²)', 'ὠ_e_z (rad/s²)', 'ὠ_d_x (rad/s²)', 'ὠ_d_y (rad/s²)', 'ὠ_d_z (rad/s²)'], 'sig_xdata': t, 'sig_ydata': [omega_e_p[:, 0], omega_e_p[:, 1], omega_e_p[:, 2], omega_d_p[0], omega_d_p[1], omega_d_p[2]], 'sig_linestyles': ['-', '-', '-', '--', '--', '--'], 'sig_colors': ['rgb(255,255,17)', 'rgb(19,159,255)', 'rgb(255,105,41)', 'rgb(0,0,255)', 'rgb(255,0,0)', 'rgb(0,127,0)']} subplot20 = {'title': 'freq per Ta step (Hz)', 'sig_labels': ['frep_per_Ta_step', 'frep_per_Ta_step_mean'], 'sig_xdata': t, 'sig_ydata': [frep_per_Ta_step, frep_per_Ta_step_mean], 'sig_linestyles': ['-', '-'], 'sig_colors': ['rgb(255,255,17)', 'rgb(255,0,0)']} subplot21 = {'title': 'quat_err', 'sig_labels': ['quat_err_2', 'quat_err_3', 'quat_err_4'], 'sig_xdata': t, 'sig_ydata': [quat_err[:, 1], quat_err[:, 2], quat_err[:, 3]], 'sig_linestyles': ['-', '-', '-', '-'], 'sig_colors': ['rgb(255,255,17)', 'rgb(19,159,255)', 'rgb(255,105,41)', 'rgb(100,212,19)']} subplot22 = {'title': 'e_ω (rad/s)', 'sig_labels': ['e_ω_x (rad/s)', 'e_ω_y (rad/s)', 'e_ω_z (rad/s)'], 'sig_xdata': t, 'sig_ydata': [omega_err[:, 0], omega_err[:, 1], omega_err[:, 2]], 'sig_linestyles': ['-', '-', '-'], 'sig_colors': ['rgb(255,255,17)', 'rgb(19,159,255)', 'rgb(255,105,41)']} subplot23 = {'title': 'ė_ω (rad/s)', 'sig_labels': ['ė_ω_x (rad/s)', 'ė_ω_y (rad/s)', 'ė_ω_z (rad/s)'], 'sig_xdata': t, 'sig_ydata': [omega_err_p[:, 0], omega_err_p[:, 1], omega_err_p[:, 2]], 'sig_linestyles': ['-', '-', '-'], 'sig_colors': ['rgb(255,255,17)', 'rgb(19,159,255)', 'rgb(255,105,41)']} subplot24 = {'title': 'tau (Nm)', 'sig_labels': ['tau_1', 'tau_2', 'tau_3', 'tau_4', 'tau_5', 'tau_6'], 'sig_xdata': t, 'sig_ydata': [tau[:, 0], tau[:, 1], tau[:, 2], tau[:, 3], tau[:, 4], tau[:, 5]], 'sig_linestyles': ['-', '-', '-', '-', '-', '-'], 'sig_colors': ['rgb(255,255,17)', 'rgb(19,159,255)', 'rgb(255,105,41)', 'rgb(100,212,19)', 'rgb(183,70,255)', 'rgb(15,255,255)']} subplot_data = [subplot1, subplot2, subplot3, subplot4, subplot5, subplot6, subplot7, subplot8, subplot9, subplot10, subplot11, subplot12, subplot13, subplot14, subplot15, subplot16, subplot17, subplot18, subplot19, subplot20, subplot21, subplot22, subplot23, subplot24] return subplot_data ```
Plotly: create html file with zoom event ```Python def plot_solution_7dof(subplot_data, save_plot=False, file_name='plot_saved', plot_fig=True, matlab_import=True): subplot_number = len(subplot_data) sig_labels = np.empty(24, dtype=object) if matlab_import: for i in range(0, subplot_number): sig_label = subplot_data[i][0][0][0][0] if len(subplot_data[i][0]) > 1: sig_labels[i] = sig_label[:-2] else: sig_labels[i] = sig_label #row=1+np.mod(i,4), col=1+int(np.floor(i/4) else: for i in range(0, subplot_number): sig_labels[i] = subplot_data[i]['title'] sig_labels_orig = sig_labels sig_labels = sig_labels.reshape(6,4).T.flatten().tolist() # Create a Plotly subplot fig = make_subplots(rows=4, cols=int(subplot_number/4), shared_xaxes=False, vertical_spacing=0.05, horizontal_spacing=0.035, subplot_titles=sig_labels) # Plot tdata for i in range(0, subplot_number): sig_title = sig_labels_orig[i] signal_number = len(subplot_data[i]['sig_ydata']) for j in range(0, signal_number): if matlab_import: sig_label = subplot_data[i][0][j][0][0] sig_xdata = subplot_data[i][0][j][1][0] sig_ydata = subplot_data[i][0][j][2][0] sig_linestyle = subplot_data[i][0][j][3][0] sig_color = 255*subplot_data[i][0][j][4][0] sig_color = f"rgb({','.join(map(str, sig_color))})" else: sig_label = subplot_data[i]['sig_labels'][j] sig_xdata = subplot_data[i]['sig_xdata'] sig_ydata = subplot_data[i]['sig_ydata'][j] sig_linestyle = subplot_data[i]['sig_linestyles'][j] sig_color = subplot_data[i]['sig_colors'][j] if sig_linestyle == '-': line_style = dict(width=1, color=sig_color, dash='solid') elif sig_linestyle == '--': line_style = dict(width=1, color=sig_color, dash='dash') act_row = 1+np.mod(i,4) act_col = 1+int(np.floor(i/4)) fig.add_trace(go.Scatter(x=sig_xdata, y=sig_ydata, name=sig_label, line = line_style, hoverinfo = 'x+y+text', hovertext=sig_label, text=sig_title), row=act_row, col=act_col) if act_row == 4: fig.update_xaxes(title_text='t (s)', row=act_row, col=act_col) # fig.update_layout(plot_bgcolor='#1e1e1e', paper_bgcolor='#1e1e1e', font=dict(color='#ffffff'), legend=dict(orientation='h')) fig.update_layout( plot_bgcolor='#101010', # Set plot background color paper_bgcolor='#1e1e1e', # Set paper background color font=dict(color='#ffffff'), # Set font color legend=dict(orientation='h', yanchor='middle', y=10, yref='container'), # Set legend orientation hovermode = 'closest', margin=dict(l=10, r=10, t=50, b=70), height=1080, # legend_indentation = 0, # margin_pad=0, # Gridline customization for all subplots **{f'xaxis{i}': dict(gridwidth=1, gridcolor='#757575', linecolor='#757575', zerolinecolor='#757575', zerolinewidth=1) for i in range(1, subplot_number+1)}, **{f'yaxis{i}': dict(gridwidth=1, gridcolor='#757575', linecolor='#757575', zerolinecolor='#757575', zerolinewidth=1) for i in range(1, subplot_number+1)} ) fig.update_layout( **{f'xaxis{i}': dict(showticklabels=False) for i in range(1, subplot_number+1-6)} ) fig.update_xaxes(matches='x', autorange=True) if(plot_fig): fig.show() if(save_plot): autoscale_code=''' rec_time = 100; //ms function autoscale_function(){ //console.log('run'); graphDiv = document.querySelector('.plotly-graph-div'); //console.log(graphDiv); if(graphDiv == null || graphDiv.on == undefined) { setTimeout(autoscale_function, rec_time); } else { var on_event=true; function test(eventdata){ //console.log(eventdata); graphDiv = document.querySelector('.plotly-graph-div'); if(on_event==true) { on_event=false; labels = graphDiv.layout.annotations; // Get changed axes yaxis_change = Object.keys(eventdata).some(key => key.includes('yaxis')); xaxis_change = Object.keys(eventdata).some(key => key.includes('xaxis')); changed_yaxis = Object.keys(eventdata).filter(key => key.includes('yaxis')); yaxis_names = changed_yaxis.map(name => name.split('.')[0]); // get only axis name yaxis_names = [...new Set(yaxis_names)]; // get unique names trace_numbers = yaxis_names.map(name => name.split('yaxis')[1]); // get trace numbers from names trace_numbers = trace_numbers.map(name => name === '' ? 0 : Number(name)-1); // trace number '' should be 0 //create a array with length of the number of traces and false for each yaxis that was not in trace_numbers // and true for each yaxis that is in trace numbers yaxis_in_trace = Array.from({length: labels.length}, (_, i) => trace_numbers.includes(i)); update={}; labels.forEach(function(act_label, i){ trace = graphDiv.data.filter(trace => trace.text.includes(labels[i].text)); xrange = graphDiv.layout.xaxis.range; yaxisName = i === 0 ? 'yaxis' : `yaxis${i + 1}`; yrange = graphDiv.layout[yaxisName].range; filteredIndices = trace[0].x.map((x, index) => x >= xrange[0] && x <= xrange[1] ? index : -1).filter(index => index !== -1); //filteredX = filteredIndices.map(index => trace[0].x[index]); g_ymax=-Infinity; g_ymin=Infinity; trace.forEach(function(el,id){ filteredY = filteredIndices.map(index => el.y[index]); if( (yaxis_change && !xaxis_change) || (yaxis_in_trace[i] && xaxis_change) ) { y_rangefilteredIndices = filteredY.map((y, index) => y >= yrange[0] && y <= yrange[1] ? index : -1).filter(index => index !== -1); filteredY = y_rangefilteredIndices.map(index => filteredY[index]); } act_min = Math.min.apply(null, filteredY); act_max = Math.max.apply(null, filteredY); g_ymax = Math.max(g_ymax, act_max); g_ymin = Math.min(g_ymin, act_min); }); offset = 1/9*(g_ymax - g_ymin)/2; if(offset == 0) { offset=0.001; } if(i == 0) { ylabel='yaxis.range'; } else { ylabel='yaxis'+(1+i)+'.range'; } update[ylabel] = [g_ymin-offset,g_ymax+offset]; }); Plotly.relayout(graphDiv, update); } else { on_event=true; } } graphDiv.on('plotly_relayout', test); } } setTimeout(autoscale_function, rec_time); ''' py.plot(fig, filename=file_name, include_mathjax='cdn', auto_open=False, include_plotlyjs='cdn') # remove include_plotlyjs='cdn' for offline only use with open(file_name, 'r', encoding='utf-8') as file: html_content = file.read() soup = BeautifulSoup(html_content, 'html.parser') first_script_tag = soup.find('script') if first_script_tag: new_script = soup.new_tag('script') new_script.string = "document.body.style.background='#1e1e1e';"+autoscale_code first_script_tag.insert_after(new_script) with open(file_name, 'w', encoding='utf-8') as file: file.write(str(soup)) webbrowser.open('file://' + file_name) ```

It's certainly not the fastest or most stable way to implement something like this (e.g., there's a bug when zooming in too close so that no sample is visible), but it works satisfactorily for me as long as there's no official solution. Attached is also an html file in which this approach has been implemented:

240910_traj2_crocddyl_mpc_thorizon_25ms.html.zip

I hope this feature will be implemented very soon.

Best regards