diff --git a/docs/_static/img/getting_started_1.png b/docs/_static/img/getting_started_1.png index eac6e57..b3de944 100644 Binary files a/docs/_static/img/getting_started_1.png and b/docs/_static/img/getting_started_1.png differ diff --git a/docs/_static/img/network_explorer_1.png b/docs/_static/img/network_explorer_1.png index 44e9b75..51a4b66 100644 Binary files a/docs/_static/img/network_explorer_1.png and b/docs/_static/img/network_explorer_1.png differ diff --git a/docs/_static/img/network_explorer_2.png b/docs/_static/img/network_explorer_2.png index 3249472..32e52f8 100644 Binary files a/docs/_static/img/network_explorer_2.png and b/docs/_static/img/network_explorer_2.png differ diff --git a/docs/_static/img/network_explorer_3.png b/docs/_static/img/network_explorer_3.png index f0b01cf..08544ba 100644 Binary files a/docs/_static/img/network_explorer_3.png and b/docs/_static/img/network_explorer_3.png differ diff --git a/docs/user_guide/network_explorer.md b/docs/user_guide/network_explorer.md index 8d7ed97..fa71ffe 100644 --- a/docs/user_guide/network_explorer.md +++ b/docs/user_guide/network_explorer.md @@ -2,7 +2,7 @@ Network explorer is interactive network explorer widget, built on pypowsybl-jupyter's widgets (SLD, NAD, Network map) and some standard [ipywidgets](https://ipywidgets.readthedocs.io/en/stable/index.html): select lists, tabs, etc. -Through the widget, you can select a voltage level from the list (or search of a specific one using the Filter) and the NAD and SLD diagrams for that voltage level will be displayed in the two "Network Area" and "Single Line" tabs, respectively. Both diagrams can be panned and zoomed. A third tab, 'Network map' displays the network's substations and lines on a map, if substations and lines geo data is available in the network. +Through the widget, you can select a voltage level from the list (or search of a specific one using the Filter) and the NAD and SLD diagrams for that voltage level will be displayed in the two "Network Area" and "Single Line" tabs, respectively. Both diagrams can be panned and zoomed. A third tab, 'Network map' displays the network's substations and lines on a map, if substations and lines geo data is available in the network. The last displayed voltage levels are listed in a history box, on the explorer's bottom left corner. The following code, to be run in a notebook, first creates a network, then displays the network explorer on it. diff --git a/js/sldwidget.ts b/js/sldwidget.ts index 4dd90e3..a3cfe26 100644 --- a/js/sldwidget.ts +++ b/js/sldwidget.ts @@ -78,10 +78,10 @@ function render({ model, el }: RenderProps) { svg_data, metadata ? JSON.parse(metadata) : null, 'voltage-level', - 500, + 800, + 600, + 800, 600, - 1000, - 1200, metadata ? handleNextVl : null, //callback on the next voltage arrows metadata ? handleSwitch : null, //callback on the breakers metadata ? handleFeeder : null, //callback on the feeders diff --git a/src/pypowsybl_jupyter/networkexplorer.py b/src/pypowsybl_jupyter/networkexplorer.py index c098cba..4b7f1de 100644 --- a/src/pypowsybl_jupyter/networkexplorer.py +++ b/src/pypowsybl_jupyter/networkexplorer.py @@ -41,7 +41,7 @@ def network_explorer(network: Network, vl_id : str = None, use_name:bool = True, network_explorer(pp.network.create_eurostag_tutorial_example1_network()) """ - sel_ctx=SelectContext(network, vl_id, use_name) + sel_ctx=SelectContext(network, vl_id, use_name, history_max_length = 10) nad_widget=None sld_widget=None @@ -61,34 +61,38 @@ def network_explorer(network: Network, vl_id : str = None, use_name:bool = True, spars=sld_parameters if sld_parameters is not None else SldParameters(use_name=use_name, nodes_infos=True) - def set_current_vl(id): - sel_ctx.extend_filtered_vls(id) - found.value=None - found.options=sel_ctx.get_filtered_vls_as_list() - sel_ctx.set_selected(id) - found.value=sel_ctx.get_selected() - def go_to_vl(event: any): arrow_vl= str(event.clicked_nextvl) - set_current_vl(arrow_vl) + if arrow_vl != sel_ctx.get_selected(): + sel_ctx.set_selected(arrow_vl, add_to_history=True) + update_select_widget(history, sel_ctx.get_selected(), sel_ctx.get_history_as_list(), on_selected_history) + update_select_widget(found, sel_ctx.get_selected() if sel_ctx.is_selected_in_filtered_vls() else None, None, on_selected) + update_explorer() + history.focus() + def toggle_switch(event: any): idswitch = event.clicked_switch.get('id') statusswitch = event.clicked_switch.get('switch_status') network.update_switches(id=idswitch, open=statusswitch) - update_sld_diagram(True) - update_nad_diagram() + update_sld_diagram(sel_ctx.get_selected(), True) + update_nad_diagram(sel_ctx.get_selected()) def go_to_vl_from_map(event: any): vl_from_map= str(event.selected_vl) - set_current_vl(vl_from_map) + if vl_from_map != sel_ctx.get_selected(): + sel_ctx.set_selected(vl_from_map, add_to_history=True) + update_select_widget(history, sel_ctx.get_selected(), sel_ctx.get_history_as_list(), on_selected_history) + update_select_widget(found, sel_ctx.get_selected() if sel_ctx.is_selected_in_filtered_vls() else None, None, on_selected) + update_explorer() + history.focus() #switch to the SLD tab tabs_diagrams.selected_index=1 - def update_nad_diagram(): + def update_nad_diagram(el): nonlocal nad_widget - if sel_ctx.get_selected() is not None: - new_diagram_data=network.get_network_area_diagram(voltage_level_ids=sel_ctx.get_selected(), + if el is not None: + new_diagram_data=network.get_network_area_diagram(voltage_level_ids=el, depth=selected_depth, high_nominal_voltage_bound=high_nominal_voltage_bound, low_nominal_voltage_bound=low_nominal_voltage_bound, nad_parameters=npars) if nad_widget==None: @@ -96,10 +100,10 @@ def update_nad_diagram(): else: update_nad(nad_widget,new_diagram_data) - def update_sld_diagram(kv: bool = False): + def update_sld_diagram(el, kv: bool = False): nonlocal sld_widget - if sel_ctx.get_selected() is not None: - sld_diagram_data=network.get_single_line_diagram(sel_ctx.get_selected(), spars) + if el is not None: + sld_diagram_data=network.get_single_line_diagram(el, spars) if sld_widget==None: sld_widget=display_sld(sld_diagram_data, enable_callbacks=True) sld_widget.on_nextvl(lambda event: go_to_vl(event)) @@ -108,23 +112,22 @@ def update_sld_diagram(kv: bool = False): else: update_sld(sld_widget, sld_diagram_data, keep_viewbox=kv, enable_callbacks=True) - def update_map(): + def update_map(el): nonlocal map_widget - if sel_ctx.get_selected() is not None: + if el is not None: if map_widget==None: - map_widget=NetworkMapWidget(network, use_name=use_name, nominal_voltages_top_tiers_filter = nominal_voltages_top_tiers_filter, use_line_geodata = use_line_geodata) + map_widget=NetworkMapWidget(network, use_name=use_name, nominal_voltages_top_tiers_filter = nominal_voltages_top_tiers_filter) map_widget.on_selectvl(lambda event : go_to_vl_from_map(event)) - else: - map_widget.center_on_voltage_level(sel_ctx.get_selected()) - + map_widget.center_on_voltage_level(el) + nadslider = widgets.IntSlider(value=selected_depth, min=0, max=20, step=1, description='depth:', disabled=False, continuous_update=False, orientation='horizontal', readout=True, readout_format='d') def on_nadslider_changed(d): nonlocal selected_depth selected_depth=d['new'] - update_nad_diagram() + update_nad_diagram(sel_ctx.get_selected()) nadslider.observe(on_nadslider_changed, names='value') @@ -134,53 +137,90 @@ def on_nadslider_changed(d): description='Filter', disabled=False, continuous_update=True, - layout=widgets.Layout(width='350px') + layout=widgets.Layout(flex='2%', height='100%', width='350px', margin='1px 0 0 0') ) + def update_select_widget(widget, el, elements=None, on_select=None): + if on_select: + widget.unobserve(on_select, names='value') + try: + if elements is not None: + widget.value = None + widget.options = elements + + widget.value = el + + finally: + if on_select: + widget.observe(on_select, names='value') + + def on_text_changed(d): nonlocal found sel_ctx.apply_filter(d['new']) - found.value=None - found.options = sel_ctx.get_filtered_vls_as_list() - if sel_ctx.is_selected_in_filtered_vls(): - found.value=sel_ctx.get_selected() + sel = sel_ctx.get_selected() if sel_ctx.is_selected_in_filtered_vls() else None + opts = sel_ctx.get_filtered_vls_as_list() + update_select_widget(found, sel, opts, on_selected) vl_input.observe(on_text_changed, names='value') found = widgets.Select( options=sel_ctx.get_filtered_vls_as_list(), - value=sel_ctx.get_selected(), + value=None, description='Found', disabled=False, - layout=widgets.Layout(width='350px', height='670px') + layout=widgets.Layout(flex='80%', height='100%', width='350px', margin='0 0 0 0') ) def on_selected(d): if d['new'] != None: - sel_ctx.set_selected(d['new']) - update_nad_diagram() - update_sld_diagram() - update_map() + sel_ctx.set_selected(d['new'], add_to_history=True) + update_select_widget(history, None, sel_ctx.get_history_as_list(), on_selected_history) + update_explorer() found.observe(on_selected, names='value') - update_nad_diagram() - update_sld_diagram() - update_map() + history = widgets.Select( + options=sel_ctx.get_history_as_list(), + value=sel_ctx.get_selected(), + description='History', + disabled=False, + layout=widgets.Layout(flex='18%', height='100%', width='350px', margin='1px 0 0 0') + ) + + def on_selected_history(d): + if d['new'] != None: + sel_ctx.set_selected(d['new'], add_to_history=False) + update_select_widget(found, sel_ctx.get_selected() if sel_ctx.is_selected_in_filtered_vls() else None, None, on_selected) + update_explorer() + + history.observe(on_selected_history, names='value') + + def update_explorer(): + sel=sel_ctx.get_selected() + update_nad_diagram(sel) + update_sld_diagram(sel) + update_map(sel) + + update_explorer() + + voltage_levels_label=widgets.Label("Voltage levels") + spacer_label=widgets.Label("") + + left_panel = widgets.VBox([vl_input, found, history], layout=widgets.Layout(width='100%', height='100%', display='flex', flex_flow='column')) - left_panel = widgets.VBox([widgets.Label('Voltage levels'), vl_input, found]) - right_panel_nad = widgets.VBox([nadslider, nad_widget]) - right_panel_sld = widgets.HBox([sld_widget]) - right_panel_map = widgets.HBox([map_widget]) + right_panel_sld = widgets.VBox([spacer_label,sld_widget]) + right_panel_map = widgets.VBox([spacer_label, map_widget]) tabs_diagrams = widgets.Tab() tabs_diagrams.children = [right_panel_nad, right_panel_sld, right_panel_map] tabs_diagrams.titles = ['Network Area', 'Single Line', 'Network map'] - tabs_diagrams.layout=widgets.Layout(width='850px', height='700px') - - hbox = widgets.HBox([left_panel, tabs_diagrams]) - hbox.layout.align_items='flex-end' + tabs_diagrams.layout=widgets.Layout(width='850px', height='700px', margin='0 0 0 4px') + + left_vbox = widgets.VBox([voltage_levels_label, left_panel]) + right_vbox = widgets.VBox([spacer_label, tabs_diagrams]) + + hbox = widgets.HBox([left_vbox, right_vbox]) - return hbox - \ No newline at end of file + return hbox \ No newline at end of file diff --git a/src/pypowsybl_jupyter/selectcontext.py b/src/pypowsybl_jupyter/selectcontext.py index 9e5b4c9..bb3a619 100644 --- a/src/pypowsybl_jupyter/selectcontext.py +++ b/src/pypowsybl_jupyter/selectcontext.py @@ -5,13 +5,13 @@ # SPDX-License-Identifier: MPL-2.0 # -from collections import OrderedDict import pandas as pd +from collections import deque from pypowsybl.network import Network class SelectContext: - def __init__(self, network:Network = None, vl_id : str = None, use_name:bool = True): + def __init__(self, network:Network = None, vl_id : str = None, use_name:bool = True, history_max_length:int = -1): self.network = network self.use_name = use_name self.display_attribute = 'name' if use_name else 'id' @@ -24,14 +24,19 @@ def __init__(self, network:Network = None, vl_id : str = None, use_name:bool = T self.apply_filter(None) + self.history = deque(maxlen=None if history_max_length == -1 else history_max_length) + self.set_selected(self.vls.index[0] if vl_id is None else vl_id) def get_vls(self): return self.vls - def set_selected(self, id): + def set_selected(self, id, add_to_history=True): if id in self.vls.index: self.selected_vl = id + last_id_from_history=self.history[0]['id'] if len(self.history)>0 else None + if add_to_history and self.selected_vl != last_id_from_history: + self.add_to_history(id) else: raise ValueError(f'a voltage level with id={id} does not exist in the network.') @@ -54,3 +59,11 @@ def get_filtered_vls_as_list(self): def extend_filtered_vls(self, id): if (id in self.vls.index) and (id not in self.vls_filtered.index): self.vls_filtered = pd.concat([self.vls_filtered, self.vls.loc[[id]]]) + + def add_to_history(self, id): + if (id in self.vls.index): + row_to_add = self.vls.loc[id].to_dict() + self.history.appendleft(row_to_add) + + def get_history_as_list(self): + return [(item[self.display_attribute], item['id']) for item in self.history]