График/Тире — Выбор легенды из графика графика не очищен правильно

#python #plotly-dash

#питон #сюжетно-тире

Вопрос:

Недавно я столкнулся с ошибкой, связанной с выбором легенды из плотных графиков в моем приложении Dash. Я работаю с несколькими вкладками, последняя из которых отображает несколько взаимосвязанных графиков и компонентов. Один из графиков рассматривается как «основной график», и выбор из этого графика приводит к изменениям в отображаемых других компонентах. При переключении на другую вкладку после того, как выбор из легенды был сделан и удален (двойным щелчком по элементу, а затем повторным двойным щелчком), а затем переключении обратно, новый выбор из легенды не будет правильно сброшен.

Я включил минимальный пример кода, чтобы воспроизвести поведение:

 import json import random import pandas as pd pd.options.plotting.backend = "plotly"  import dash from dash.dependencies import Input, Output, State import dash_table as dt import dash_core_components as dcc import dash_html_components as html import dash_bootstrap_components as dbc   external_stylesheets = ['https://codepen.io/chriddyp/pen/bWLwgP.css',  "https://codepen.io/chriddyp/pen/brPBPO.css"] app = dash.Dash(__name__, external_stylesheets=external_stylesheets)  # -------------------- DATA --------------------  plot_df = pd.DataFrame({  'x': [1,2,3,4,5],  'y': [1,2,3,4,5],  'text': ['a', 'b', 'c', 'd', 'e'],  'label': [-1, -1, -1, -1, -1]  })  # -------------------- LAYOUTS -------------------- tab1_layout = html.Div(  [  html.H1('Tab 1'),  html.P('Please navigate to tab 2.')  ] )  tab2_layout = html.Div(  [  html.Div([html.H1('Tab 2')]),   # Slider  html.Div(  [  html.H3(  children=['Slider'],  ),   dcc.Slider(  id="slider",  min=2,  max=5,  step=1,  value=2,  marks={  2: "2",  5: "5",  },  disabled=False,  ),  ]  ),   # Graph  html.Div(  [  html.H3(  children=['Graph'],  ),   dcc.Graph(  id="graph",  config={  'modeBarButtonsToRemove': ['autoScale2d', 'zoomIn2d', 'zoomOut2d', 'resetScale2d', 'hoverClosestCartesian', 'hoverCompareCartesian', 'toggleSpikelines'],  'displaylogo': False  }  ),  ]  ),   # DataTable  html.Div(  [  html.H3(  children=['DataTable'],  ),   dt.DataTable(  id='data_table',  sort_action="native",  page_action="native",  page_current= 0,  page_size= 100,  style_as_list_view=True  ),  ]  ),  ],  style={  'width':'50%',  },  )  app.layout = html.Div(  [   dcc.Store(id='store_data', data=plot_df.to_json()),  dcc.Store(id='displayed_legends'),  dcc.Tabs(  id='main_tabs',   value='tab1',  children=[  dcc.Tab(id="tab_1", label='1. Tab 1', value='tab1', children=[tab1_layout]),  dcc.Tab(id="tab_2", label='2. Tab 2', value='tab2', children=[tab2_layout])  ],  ),  ] )  # -------------------- CALLBACKS --------------------  @app.callback( [Output('graph', 'figure'), Output('store_data', 'data')],  Input('slider', 'value'),  State('store_data', 'data') ) def update_graph_from_slider(n, data_json) :  plot_df = pd.read_json(data_json)   labels_int = list(range(0, n))  k = len(plot_df) - len(labels_int)  if k gt; 0:  labels_int  = random.choices(range(0, n), k=k)  labels = [str(e) for e in labels_int]   plot_df['label'] = labels   plot_df = plot_df.sort_values(by='label', ascending=True)  plot_fig = plot_df.plot.scatter(x='x', y='y', hover_data={'x': False, 'y': False, 'label': True, 'text': True} ,color='label')   return plot_fig, plot_df.to_json()  @app.callback(  Output('displayed_legends', 'data'),  Input('graph', 'restyleData'),  State('displayed_legends', 'data') ) def update_selected_legend_items(restyleEvent, displayedLegendsItems):  if displayedLegendsItems == None:  displayedLegendsItems = {}  if restyleEvent != None:  states = restyleEvent[0]['visible']  labels = restyleEvent[1]  for index in range(len(restyleEvent[0]['visible'])):  if states[index] == 'legendonly':#legend item has been deactivated  displayedLegendsItems[labels[index]] = False  elif states[index] == True:#legend item has been activated  displayedLegendsItems[labels[index]] = True  return displayedLegendsItems  @app.callback( [Output('data_table', 'columns'), Output('data_table', 'data'),],  [Input('graph', 'selectedData'), Input('graph', 'relayoutData'), Input('displayed_legends', 'data')],  State('store_data', 'data'), ) def display_selected_data(selectedData, relayoutData, selectedTopics, data_json):  res = get_filtered_nodes(data_json, selectedTopics, selectedData)  df_table = pd.DataFrame(res)  visibility = {}  changed_id = [p['prop_id'] for p in dash.callback_context.triggered][0]  if 'graph' in changed_id:  if 'relayoutData' in changed_id:  relayoutDataKeys = relayoutData.keys()  # if we zoomed, auto zoomed or rescaled,don't update the list  if 'dragmode' in relayoutDataKeys or 'xaxis.range[0]' in relayoutDataKeys or 'xaxis.autorange' in relayoutDataKeys:  res = dash.no_update  if res != dash.no_update and len(res) == 0:  visibility = {'display':'none'}   return [{"name": i, "id": i} for i in df_table.columns], df_table.to_dict('records')  # -------------------- FUNCTIONS -------------------- def get_filtered_nodes(data, selectedTopics, selectedData):  res = []  selectedTopicsKeys = selectedTopics.keys()   if selectedData != None:#There is an active selection  pointsList = json.loads(json.dumps(selectedData))  if pointsList != None and len(pointsList['points']) gt; 0:#Some points are selected  for point in pointsList['points']:  text = point['customdata'][1]  label = point['customdata'][0]  if label not in selectedTopicsKeys or selectedTopics[label] == True:  res.append({'text': text, 'label': label})  elif data != None:#No active selection  dataArray = json.loads(data)  textArray = dataArray['text']  if 'label' in dataArray :  labelsArray = dataArray['label']   for key in textArray.keys():  if labelsArray[key] not in selectedTopicsKeys or selectedTopics[labelsArray[key]] == True:  res.append({'text': textArray[key], 'label': labelsArray[key]})  return res   if __name__ == "__main__":  app.run_server(debug=True)  

Используемые библиотеки (и их версии):

  • панды==1.3.3
  • тире==1.20.0
  • dash-основные компоненты==1.16.0
  • тире-начальная загрузка-компоненты==0.12.2
  • dash-html-компоненты==1.1.3
  • тире-таблица==4.11.3 Я использую python 3.9.

И вот подробный шаг для репликации ошибки:

  • запустите приложение как обычно
  • перейдите на вкладку 2
  • выберите любой элемент из легенды (дважды щелкнув по нему).
  • clear selection (by double-clicking anywhere on the legend again)
  • go to tab 1
  • go back to tab 2
  • select any item from the legend (by double-clicking on it)
  • clear selection by double-clicking again: the datatable is not updated
  • optional: select new value from slider
  • optional: select any item from the legend (by double-clicking on it)
  • необязательно: снимите выделение, дважды щелкнув еще раз: на этот раз все работает так, как задумано (из моих экспериментов следует, что ошибка влияет только на последнее значение, в котором был сделан выбор перед переключением вкладок).

Немного изучив эту ошибку, выясняется, что свойство restyleData из графика, которое обычно используется для обнаружения изменения выбора из легенды, не обновляется при очистке выбора и, таким образом, сохраняет ранее выбранный элемент. Любая информация была бы очень признательна 🙂