def test_cbmt012_initialization_with_overlapping_outputs(generate, dash_duo): app = Dash(__name__, suppress_callback_exceptions=generate) block = html.Div( [ html.Div(id="input-1", children="input-1"), html.Div(id="input-2", children="input-2"), html.Div(id="input-3", children="input-3"), html.Div(id="input-4", children="input-4"), html.Div(id="input-5", children="input-5"), html.Div(id="output-1"), html.Div(id="output-2"), html.Div(id="output-3"), html.Div(id="output-4"), ] ) call_counts = { "container": Value("i", 0), "output-1": Value("i", 0), "output-2": Value("i", 0), "output-3": Value("i", 0), "output-4": Value("i", 0), } if generate: app.layout = html.Div([html.Div(id="input"), html.Div(id="container")]) @app.callback(Output("container", "children"), Input("input", "children")) def set_content(_): call_counts["container"].value += 1 return block else: app.layout = block def generate_callback(outputid): def callback(*args): call_counts[outputid].value += 1 return "{}, {}".format(*args) return callback for i in range(1, 5): outputid = "output-{}".format(i) app.callback( Output(outputid, "children"), Input("input-{}".format(i), "children"), Input("input-{}".format(i + 1), "children"), )(generate_callback(outputid)) dash_duo.start_server(app) for i in range(1, 5): outputid = "output-{}".format(i) dash_duo.wait_for_text_to_equal( "#{}".format(outputid), "input-{}, input-{}".format(i, i + 1) ) assert call_counts[outputid].value == 1 assert call_counts["container"].value == (1 if generate else 0)
def test_generate_overlapping_outputs(self): app = Dash() app.config["suppress_callback_exceptions"] = True block = html.Div([ html.Div(id="input-1", children="input-1"), html.Div(id="input-2", children="input-2"), html.Div(id="input-3", children="input-3"), html.Div(id="input-4", children="input-4"), html.Div(id="input-5", children="input-5"), html.Div(id="output-1"), html.Div(id="output-2"), html.Div(id="output-3"), html.Div(id="output-4"), ]) app.layout = html.Div([html.Div(id="input"), html.Div(id="container")]) call_counts = { "container": Value("i", 0), "output-1": Value("i", 0), "output-2": Value("i", 0), "output-3": Value("i", 0), "output-4": Value("i", 0), } @app.callback(Output("container", "children"), [Input("input", "children")]) def display_output(*args): call_counts["container"].value += 1 return block def generate_callback(outputid): def callback(*args): call_counts[outputid].value += 1 return "{}, {}".format(*args) return callback for i in range(1, 5): outputid = "output-{}".format(i) app.callback( Output(outputid, "children"), [ Input("input-{}".format(i), "children"), Input("input-{}".format(i + 1), "children"), ], )(generate_callback(outputid)) self.startServer(app) wait_for(lambda: call_counts["container"].value == 1) self.wait_for_element_by_css_selector("#output-1") time.sleep(5) for i in range(1, 5): outputid = "output-{}".format(i) self.assertEqual(call_counts[outputid].value, 1) self.wait_for_text_to_equal("#{}".format(outputid), "input-{}, input-{}".format(i, i + 1)) self.assertEqual(call_counts["container"].value, 1)
def test_module_component_prefix(): app = Dash(__name__) div = html.H2('Hello World', id='hello') app.callback(div.output.children, div.input.n_clicks) def _callback(clicks): return None assert div.id == 'tests-spa-spa_prefix_test-hello'
def add_routing_callback(dash: Dash) -> None: """Add routing callback""" dash.callback( Output("row-main", "children"), [Input("url", "pathname")] )(create_page) dash.callback( Output("url", "pathname"), [Input("btn-next", "n_clicks"), Input("btn-prev", "n_clicks")], [State("url", "pathname")] )(update_url)
def add_callback(self, app: dash.Dash, func: Callable, output: Output, inputs: Sequence[Input], state: Sequence[State] = tuple(), prevent_initial_call: Optional[bool] = None) -> None: if output.component_id in self._callback_output_ids: return self._callback_output_ids.append(output.component_id) app.callback(output, inputs, state, prevent_initial_call=prevent_initial_call)(func)
def test_initialization_with_overlapping_outputs(self): app = Dash() app.layout = html.Div( [ html.Div(id="input-1", children="input-1"), html.Div(id="input-2", children="input-2"), html.Div(id="input-3", children="input-3"), html.Div(id="input-4", children="input-4"), html.Div(id="input-5", children="input-5"), html.Div(id="output-1"), html.Div(id="output-2"), html.Div(id="output-3"), html.Div(id="output-4"), ] ) call_counts = { "output-1": Value("i", 0), "output-2": Value("i", 0), "output-3": Value("i", 0), "output-4": Value("i", 0), } def generate_callback(outputid): def callback(*args): call_counts[outputid].value += 1 return "{}, {}".format(*args) return callback for i in range(1, 5): outputid = "output-{}".format(i) app.callback( Output(outputid, "children"), [ Input("input-{}".format(i), "children"), Input("input-{}".format(i + 1), "children"), ], )(generate_callback(outputid)) self.startServer(app) self.wait_for_element_by_css_selector("#output-1") time.sleep(5) for i in range(1, 5): outputid = "output-{}".format(i) self.assertEqual(call_counts[outputid].value, 1) self.wait_for_text_to_equal( "#{}".format(outputid), "input-{}, input-{}".format(i, i + 1) )
def apply(cls, app: Dash) -> None: for cb in cls.REGISTRY: try: if isinstance(cb, deferred_callback): LOG.info(f"Applying deferred callback {cb.f.__name__}") app.callback(*cb.args, **cb.kwargs)(cb.f) else: LOG.info( f"Applying deferred clientside callback {cb.name}") app.clientside_callback(*cb.args, **cb.kwargs) except Exception as e: LOG.error(f"Callback {type(cb)=}, {cb=}, " f"{getattr(cb, 'name', None)=}" f"{getattr(cb, 'f', None)=}" f"failed") LOG.error(e) continue
def register_callbacks(dash: Dash, PageClass: Type[Page], method: Callable[..., Any]): outputs, inputs, states = method.callback_parameters # type: ignore n_outputs = len(outputs) method_name = method.__name__ registration_hooks: Iterable[RegistrationHookType] = getattr( method, "registration_hooks", None) or [] for hook in registration_hooks: hook(PageClass, method_name, outputs, inputs, states) if isinstance(inspect.getattr_static(PageClass, method_name), staticmethod): # Callback is staticmethod. callback = make_static_callback(PageClass, method, method_name, n_outputs) elif isinstance(inspect.getattr_static(PageClass, method_name), classmethod): # Callback is classmethod. callback = make_class_callback(PageClass, method, method_name, n_outputs) elif not method.is_mutating: # type: ignore # Callback is non-mutating instance method. callback = make_non_mutating_callback(PageClass, method, method_name, n_outputs) # Add session store to states. states.append(State(PageClass._store_name, "data")) else: # Callback is mutating instance method. callback = make_mutating_callback(PageClass, method, method_name, n_outputs) # Collect callback store and add to outputs. callback_store_name = PageClass._store_name + f"-callback-{method_name}" PageClass._callback_stores.append(callback_store_name) outputs.append(Output(callback_store_name, "data")) # Add session store to states. states.append(State(PageClass._store_name, "data")) # Collect callback's error div and add to the output. error_div_name = f"page-{PageClass.route}-error-div-{method_name}" PageClass._callback_error_divs.append(error_div_name) outputs.append(Output(error_div_name, "children")) # Register callback. dash.callback(outputs, inputs, states)(callback)
class Dashboard: """This is abstract dashboard method Every nested class should implement following abstract methods: - _set_layout: to set dash.app.layout property # TODO: expand the class with default layout # TODO: expand the class with default styling """ def __init__(self, mode='default', **kwargs): external_stylesheets = [BOOTSTRAP] if mode == 'jupyter': self.app = JupyterDash(external_stylesheets=external_stylesheets) else: self.app = Dash(external_stylesheets=external_stylesheets) app = self.app # For referencing with the decorator (see line below) app.title = 'CEHS Uganda' @app.server.route('/static/<asset_type>/<path>') def static_file(asset_type, path): return send_from_directory(here / 'static' / asset_type, path) ################ # LAYOUT # ################ def _set_layout(self): """Method is left deliberately empty. Every child class should implement this class""" raise NotImplementedError( 'Every child class should implement __set_layout method!') ################### # EXECUTION # ################### def run(self, dev=False, **kwargs): self.set_layout_and_callbacks() self.app.run_server(debug=dev, use_reloader=dev, **kwargs) def set_layout_and_callbacks(self): self._set_layout() self._define_callbacks() def switch_data_set(self, data): for x in self.data_cards: if isinstance(x, DataCard) or getattr(x, 'data') is not None: try: x.data = data # x.figure = x._get_figure(x.data) except AttributeError as e: print(e) ################### # CALLBACKS # ################### def _define_callbacks(self): # TODO: self.data_cards is property of datastory... Move this to datastory or data_cards to dashboard? # Datacard level for x in self.data_cards: if x._requires_dropdown(): for callback in x.callbacks: self.register_callback( callback.get('input'), callback.get('output'), callback.get('func')) for x in self.ind_elements: # FIXME if x._requires_dropdown(): for callback in x.callbacks: self.register_callback( callback.get('input'), callback.get('output'), callback.get('func')) def register_callback(self, input_element_params, output_elements_params, function): out_set, in_set = self.__define_callback_set( output_elements_params, input_element_params) callback_function = self.__process_callback_function(function) self.app.callback(inputs=in_set, output=out_set)(callback_function) def __process_callback_function(self, function): def callback_wrapper(*input_values): value = function(*input_values) return value return callback_wrapper def __define_callback_set(self, output_elements_id_prop: [(str, str)], input_element_id_prop: [(str, str)] ): callback_set_outputs = [ Output(component_id=component_id, component_property=component_prop) for component_id, component_prop in output_elements_id_prop ] callback_set_input = [ Input(component_id=component_id, component_property=component_prop) for component_id, component_prop in input_element_id_prop] return (callback_set_outputs, callback_set_input)
APP.layout = html.Div([ navbar, dcc.Location(id='url', refresh=False), html.Div(className="container main-container", children=[]) authorized_emails=[] auth=GoogleOAuth( APP, authorized_emails, ) @APP.callback( Output('username', 'children'), [Input('placeholder', 'value')] ) def on_load(value): return "{},".format(session['email']) @APP.callback(dash.dependencies.Output('page-content', 'children'), [dash.dependencies.Input('url', 'pathname')]) def display_page(pathname): if pathname == '/': return wp.generate_page_1_layout() elif pathname == '/logout': resp=google.post(
dcc.Input(id='input-c', type="text"), html.Div( [ 'Timestamps:', html.Br(), 'A: ', html.Span(id='input-a-timestamp'), html.Br(), 'B: ', html.Span(id='input-b-timestamp'), html.Br(), 'C: ', html.Span(id='input-c-timestamp'), html.Br(), ], style={'display': 'block'} # Switch this on or off for debugging. ), html.Span(['and the latest value is: ']), html.Span(id='latest-value') ]) app.callback( Output('input-a-timestamp', 'children'), [Input('input-a', 'value')] )(update_timestamp) app.callback( Output('input-b-timestamp', 'children'), [Input('input-b', 'value')] )(update_timestamp) app.callback( Output('input-c-timestamp', 'children'), [Input('input-c', 'value')] )(update_timestamp) app.callback( Output('latest-value', 'children'), [Input('input-a-timestamp', 'children'), Input('input-b-timestamp', 'children'), Input('input-c-timestamp', 'children')], [State('input-a', 'value'), State('input-b', 'value'),
def init_dash(flask_app: Flask, config: Config): logger = flask_app.logger dash = Dash( __name__, server=flask_app, routes_pathname_prefix="/", external_stylesheets=[ # dbc.themes.BOOTSTRAP, # "https://use.fontawesome.com/releases/v5.8.1/css/all.css" ], meta_tags=[ { "charset": "utf-8" }, { "name": "viewport", "content": "width=device-width, initial-scale=1, shrink-to-fit=no", }, ], ) dash.config["suppress_callback_exceptions"] = True dash.title = config.project.display_name or config.project.name dash.layout = html.Div(children=[ # Represents the URL bar, doesn't render anything. dcc.Location(id="url", refresh=False), dbc.NavbarSimple( id="navbar-content", brand="AllenNLP Manager", brand_href="/", sticky="top", color="#162328", dark=True, ), dcc.Store(id="current-path"), dbc.Container(id="page-content"), dbc.Container( id="notifications", style={ "position": "fixed", "top": 66, "right": 10, "width": 350 }, children=[ html.Div(id="page-loading-error"), html.Div(id="page-notifications"), html.Div(id="page-callback-errors"), ], ), ]) # Import all dashboard pages so that they get registered. import_submodules("mallennlp.dashboard") for module in config.server.imports or []: logger.info("Importing additional module %s", module) import_submodules(module) additional_navlinks = [] for page_name in Page.list_available(): PageClass = Page.by_name(page_name) if PageClass.navlink_name is not None: additional_navlinks.append( dbc.DropdownMenuItem( dcc.Link(PageClass.navlink_name, href=page_name))) Page.logger = logger # Define callback to render navbar. @dash.callback(Output("navbar-content", "children"), [Input("url", "pathname")]) def render_navbar(pathname): source_link = dbc.NavItem( dbc.NavLink( [html.I(className="fab fa-github"), " Source"], href="https://github.com/epwalsh/allennlp-manager", )) if current_user.is_authenticated: menu_items = [ dbc.DropdownMenuItem( ["Signed in as ", html.Strong(current_user.username)], disabled=True), html.Hr(), dbc.DropdownMenuItem(dcc.Link("Home", href="/")), dbc.DropdownMenuItem(dcc.Link("System info", href="/sys-info")), ] if additional_navlinks: menu_items.append(html.Hr()) menu_items.extend(additional_navlinks) menu_items.extend([ html.Hr(), dbc.DropdownMenuItem(dcc.Link("Settings", href="/settings")), dbc.DropdownMenuItem( dcc.Link("Logout", href="/logout", refresh=True)), ]) return [ source_link, dbc.DropdownMenu(nav=True, in_navbar=True, label="Menu", children=menu_items), ] return [ source_link, dbc.NavItem( dbc.NavLink("Sign in", href="/login", external_link=True)), ] # Define callback to render pages. Takes the URL path and get the corresponding # page. @dash.callback( [ Output("page-content", "children"), Output("page-callback-errors", "children"), Output("page-notifications", "children"), Output("page-loading-error", "children"), Output("current-path", "data"), ], [Input("url", "pathname"), Input("url", "search")], [State("current-path", "data")], ) def render_page(pathname: str, param_string: str, current_path_data): if pathname is None: raise PreventUpdate logger.debug("Attempting to render page %s", pathname) if pathname != "/" and pathname.endswith("/") or pathname.endswith( "#"): pathname = pathname[:-1] if current_path_data: # If nothing in the path / param_string has changed, don't actually do anything. # NOTE: this is kind of a hack, since sometimes we have buttons w/ href='#', # and we don't want to actually re-render the pages content when clicked. if (current_path_data["pathname"] == pathname and current_path_data["param_string"] == param_string): raise PreventUpdate updated_data = {"pathname": pathname, "param_string": param_string} try: PageClass = Page.by_name(pathname) if PageClass.permissions: if not current_user.is_authenticated: PageClass = Page.by_name("/login") params = PageClass.Params(next_pathname=pathname, next_params=param_string) return PageClass.from_params(params).render() + ( None, updated_data) if PageClass.permissions > current_user.permissions: raise NotPermittedError( "You do not have adequate permissions to view this page" ) params = from_url(PageClass.Params, param_string) return PageClass.from_params(params).render() + (None, updated_data) except RegistrationError: return ( [], [], [], dbc.Toast(f"Page {pathname} not found", header="404", icon="danger"), updated_data, ) except InvalidPageParametersError as e: return ( [], [], [], dbc.Toast(str(e), header="Bad page parameters", icon="danger"), updated_data, ) except Exception as e: logger.exception(e) return ( [], [], [], dbc.Toast(str(e), header=e.__class__.__name__, icon="danger"), updated_data, ) # Now we loop through all registered pages and register their callbacks # with the dashboard application. for page_name in Page.list_available(): PageClass = Page.by_name(page_name) PageClass.route = page_name if getattr(PageClass, "logger", None) is None: PageClass.logger = logger PageClass._store_name = f"page-{page_name}-store" PageClass._callback_stores = [] PageClass._callback_error_divs = [] for _, method in filter(lambda x: callable(x[1]), inspect.getmembers(PageClass)): if not getattr(method, "is_callback", False): continue register_callbacks(dash, PageClass, method) if PageClass._callback_stores: dash.callback( Output(PageClass._store_name, "data"), [ Input(s, "modified_timestamp") for s in PageClass._callback_stores ], [State(s, "data") for s in PageClass._callback_stores], )(store_callback) return dash
def test_radio_buttons_callbacks_generating_children(self): self.maxDiff = 100 * 1000 app = Dash(__name__) app.layout = html.Div([ dcc.RadioItems(options=[{ 'label': 'Chapter 1', 'value': 'chapter1' }, { 'label': 'Chapter 2', 'value': 'chapter2' }, { 'label': 'Chapter 3', 'value': 'chapter3' }, { 'label': 'Chapter 4', 'value': 'chapter4' }, { 'label': 'Chapter 5', 'value': 'chapter5' }], value='chapter1', id='toc'), html.Div(id='body') ]) for script in dcc._js_dist: app.scripts.append_script(script) chapters = { 'chapter1': html.Div([ html.H1('Chapter 1', id='chapter1-header'), dcc.Dropdown(options=[{ 'label': i, 'value': i } for i in ['NYC', 'MTL', 'SF']], value='NYC', id='chapter1-controls'), html.Label(id='chapter1-label'), dcc.Graph(id='chapter1-graph') ]), # Chapter 2 has the some of the same components in the same order # as Chapter 1. This means that they won't get remounted # unless they set their own keys are differently. # Switching back and forth between 1 and 2 implicitly # tests how components update when they aren't remounted. 'chapter2': html.Div([ html.H1('Chapter 2', id='chapter2-header'), dcc.RadioItems(options=[{ 'label': i, 'value': i } for i in ['USA', 'Canada']], value='USA', id='chapter2-controls'), html.Label(id='chapter2-label'), dcc.Graph(id='chapter2-graph') ]), # Chapter 3 has a different layout and so the components # should get rewritten 'chapter3': [ html.Div( html.Div([ html.H3('Chapter 3', id='chapter3-header'), html.Label(id='chapter3-label'), dcc.Graph(id='chapter3-graph'), dcc.RadioItems(options=[{ 'label': i, 'value': i } for i in ['Summer', 'Winter']], value='Winter', id='chapter3-controls') ])) ], # Chapter 4 doesn't have an object to recursively # traverse 'chapter4': 'Just a string', # Chapter 5 contains elements that are bound with events 'chapter5': [ html.Div([ html.Button(id='chapter5-button'), html.Div(id='chapter5-output') ]) ] } call_counts = { 'body': Value('i', 0), 'chapter1-graph': Value('i', 0), 'chapter1-label': Value('i', 0), 'chapter2-graph': Value('i', 0), 'chapter2-label': Value('i', 0), 'chapter3-graph': Value('i', 0), 'chapter3-label': Value('i', 0), 'chapter5-output': Value('i', 0) } @app.callback(Output('body', 'children'), [Input('toc', 'value')]) def display_chapter(toc_value): call_counts['body'].value += 1 return chapters[toc_value] app.config.supress_callback_exceptions = True def generate_graph_callback(counterId): def callback(value): call_counts[counterId].value += 1 return { 'data': [{ 'x': ['Call Counter'], 'y': [call_counts[counterId].value], 'type': 'bar' }], 'layout': { 'title': value } } return callback def generate_label_callback(id): def update_label(value): call_counts[id].value += 1 return value return update_label for chapter in ['chapter1', 'chapter2', 'chapter3']: app.callback(Output('{}-graph'.format(chapter), 'figure'), [Input('{}-controls'.format(chapter), 'value')])( generate_graph_callback( '{}-graph'.format(chapter))) app.callback(Output('{}-label'.format(chapter), 'children'), [Input('{}-controls'.format(chapter), 'value')])( generate_label_callback( '{}-label'.format(chapter))) chapter5_output_children = 'Button clicked' @app.callback(Output('chapter5-output', 'children'), events=[Event('chapter5-button', 'click')]) def display_output(): call_counts['chapter5-output'].value += 1 return chapter5_output_children self.startServer(app) time.sleep(0.5) wait_for(lambda: call_counts['body'].value == 1) wait_for(lambda: call_counts['chapter1-graph'].value == 1) wait_for(lambda: call_counts['chapter1-label'].value == 1) self.assertEqual(call_counts['chapter2-graph'].value, 0) self.assertEqual(call_counts['chapter2-label'].value, 0) self.assertEqual(call_counts['chapter3-graph'].value, 0) self.assertEqual(call_counts['chapter3-label'].value, 0) def generic_chapter_assertions(chapter): # each element should exist in the dom paths = self.driver.execute_script( 'return window.store.getState().paths') for key in paths: self.driver.find_element_by_id(key) if chapter == 'chapter3': value = chapters[chapter][0]['{}-controls'.format( chapter)].value else: value = chapters[chapter]['{}-controls'.format(chapter)].value # check the actual values wait_for(lambda: (self.driver.find_element_by_id('{}-label'.format( chapter)).text == value)) wait_for(lambda: (self.driver.execute_script( 'return document.' 'getElementById("{}-graph").'.format(chapter) + 'layout.title') == value)) self.assertEqual( self.driver.execute_script( 'return window.store.getState().requestQueue'), []) def chapter1_assertions(): paths = self.driver.execute_script( 'return window.store.getState().paths') self.assertEqual( paths, { 'toc': ['props', 'children', 0], 'body': ['props', 'children', 1], 'chapter1-header': [ 'props', 'children', 1, 'props', 'children', 'props', 'children', 0 ], 'chapter1-controls': [ 'props', 'children', 1, 'props', 'children', 'props', 'children', 1 ], 'chapter1-label': [ 'props', 'children', 1, 'props', 'children', 'props', 'children', 2 ], 'chapter1-graph': [ 'props', 'children', 1, 'props', 'children', 'props', 'children', 3 ] }) generic_chapter_assertions('chapter1') chapter1_assertions() # switch chapters (self.driver.find_elements_by_css_selector('input[type="radio"]')[1] ).click() # sleep just to make sure that no calls happen after our check time.sleep(2) wait_for(lambda: call_counts['body'].value == 2) wait_for(lambda: call_counts['chapter2-graph'].value == 1) wait_for(lambda: call_counts['chapter2-label'].value == 1) self.assertEqual(call_counts['chapter1-graph'].value, 1) self.assertEqual(call_counts['chapter1-label'].value, 1) def chapter2_assertions(): paths = self.driver.execute_script( 'return window.store.getState().paths') self.assertEqual( paths, { 'toc': ['props', 'children', 0], 'body': ['props', 'children', 1], 'chapter2-header': [ 'props', 'children', 1, 'props', 'children', 'props', 'children', 0 ], 'chapter2-controls': [ 'props', 'children', 1, 'props', 'children', 'props', 'children', 1 ], 'chapter2-label': [ 'props', 'children', 1, 'props', 'children', 'props', 'children', 2 ], 'chapter2-graph': [ 'props', 'children', 1, 'props', 'children', 'props', 'children', 3 ] }) generic_chapter_assertions('chapter2') chapter2_assertions() # switch to 3 (self.driver.find_elements_by_css_selector('input[type="radio"]')[2] ).click() # sleep just to make sure that no calls happen after our check time.sleep(2) wait_for(lambda: call_counts['body'].value == 3) wait_for(lambda: call_counts['chapter3-graph'].value == 1) wait_for(lambda: call_counts['chapter3-label'].value == 1) self.assertEqual(call_counts['chapter2-graph'].value, 1) self.assertEqual(call_counts['chapter2-label'].value, 1) self.assertEqual(call_counts['chapter1-graph'].value, 1) self.assertEqual(call_counts['chapter1-label'].value, 1) def chapter3_assertions(): paths = self.driver.execute_script( 'return window.store.getState().paths') self.assertEqual( paths, { 'toc': ['props', 'children', 0], 'body': ['props', 'children', 1], 'chapter3-header': [ 'props', 'children', 1, 'props', 'children', 0, 'props', 'children', 'props', 'children', 0 ], 'chapter3-label': [ 'props', 'children', 1, 'props', 'children', 0, 'props', 'children', 'props', 'children', 1 ], 'chapter3-graph': [ 'props', 'children', 1, 'props', 'children', 0, 'props', 'children', 'props', 'children', 2 ], 'chapter3-controls': [ 'props', 'children', 1, 'props', 'children', 0, 'props', 'children', 'props', 'children', 3 ] }) generic_chapter_assertions('chapter3') chapter3_assertions() # switch to 4 (self.driver.find_elements_by_css_selector('input[type="radio"]')[3] ).click() wait_for(lambda: (self.driver.find_element_by_id('body').text == 'Just a string')) # each element should exist in the dom paths = self.driver.execute_script( 'return window.store.getState().paths') for key in paths: self.driver.find_element_by_id(key) self.assertEqual(paths, { 'toc': ['props', 'children', 0], 'body': ['props', 'children', 1] }) # switch back to 1 (self.driver.find_elements_by_css_selector('input[type="radio"]')[0] ).click() time.sleep(0.5) chapter1_assertions() # switch to 5 (self.driver.find_elements_by_css_selector('input[type="radio"]')[4] ).click() time.sleep(1) # click on the button and check the output div before and after chapter5_div = lambda: self.driver.find_element_by_id('chapter5-output' ) chapter5_button = lambda: self.driver.find_element_by_id( 'chapter5-button') self.assertEqual(chapter5_div().text, '') chapter5_button().click() wait_for(lambda: chapter5_div().text == chapter5_output_children) time.sleep(0.5) self.assertEqual(call_counts['chapter5-output'].value, 1)
#http://localhost:5000/d/DisplayScreen@screen=ResultOverview&asset=Alperia-VSM #http://localhost:5000/d/DisplayScreen@screen=test&asset=Alperia-VSM def getScreenVariables(user): def getScreenVariablesForUser(): return dictionaryOfAllScreenVariables[user] return getScreenVariablesForUser interactionsDict = parse("001") callbackFunctions = compile_callbacks("001", getScreenVariables("002")) for interaction in interactionsDict: inputLst = [] for input in interaction['input']: inputId = input['object'] + '-' + input['type'] + '-' + interaction[ 'screen'] if input['type'] == 'CDatePicker': inputLst.append(Input('date-picker-range-' + inputId, 'start_date')) inputLst.append(Input('date-picker-range-' + inputId, 'end_date')) else: inputLst.append(Input(inputId, 'value')) outputId = interaction['output']['object'] + '-' + interaction['output'][ 'type'] + '-' + interaction['screen'] dash_app.callback(Output( outputId, interaction['output']['param']), inputLst)( callbackFunctions[interaction['screen']][interaction['callback']])
def add_slide2_callbacks(dash: Dash) -> None: """Add routing callback""" dash.callback(Output("slide2-plot", "children"), [Input("url", "pathname")])(update_plot)
elif state['type'] == 'CChecklist': stateLst.append(State(stateId, 'values')) elif state['type'] == 'CRadioItems': stateLst.append(State(stateId, 'value')) elif state['type'] == 'CButton': stateLst.append(State(stateId, 'n_clicks')) elif state['type'] == 'CUpload': stateLst.append(State(stateId, 'contents')) stateLst.append(State(stateId, 'filename')) elif state['type'] == 'CTabs': stateLst.append(State(stateId, 'value')) elif state['type'] == 'CDataTable': stateLst.append(State(stateId, 'selected_row_indices')) elif state['type'] == 'CChart': stateLst.append(State(stateId, 'figure')) elif state['type'] == 'CMap': stateLst.append(State(stateId, 'figure')) elif state['type'] == 'CTopologyMap': stateLst.append(State(stateId, 'clickData')) elif state['type'] == 'CInterval': state.append(State(stateId, 'n_intervals')) ''' outputId = generateId(interaction['output']['object'], interaction['output']['type'], interaction['screen']) dash_app.callback( Output(outputId, interaction['output']['param']), inputLst, stateLst, )(callbackFunctions[interaction['screen']][interaction['callback']] if interaction['callback'] in callbackFunctions[interaction['screen']] else globalCallbacks[interaction['callback']])
@app.callback(Output('stats-container', 'children'), [Input('replayUpload', 'contents'), Input('replayUpload', 'filename'), Input('replayUpload', 'last_modified')], [State('aliases', 'value')]) def update_output_div(list_of_contents, list_of_names, list_of_dates, aliases): # if(n_clicks > 0): if(list_of_names): replays = [load_replay(c, n, d) for c, n, d in zip(list_of_contents, list_of_names, list_of_dates)] (stats, rep_list) = get_statistics(replays, [aliases]) return get_stats_layout(stats, rep_list) app.callback(Output('full-game-list-content', 'style'), [Input('tabs', 'value')])( create_full_game_list_tab_callback()) for race in list(ALL_RACES.keys()): app.callback(Output(f'{race}-content', 'style'), [Input('tabs', 'value')])( create_mainrace_tab_callback(race)) app.callback(Output(f'{race}-content-total', 'style'), [Input(f'{race}-tabs', 'value')])( create_total_tab_callback(race)) app.callback(Output(f'{race}-content-race', 'style'), [Input(f'{race}-tabs', 'value')])( create_race_tab_callback(race)) app.callback(Output(f'{race}-content-map', 'style'), [Input(f'{race}-tabs', 'value')])( create_map_tab_callback(race)) for enemy_race in list(ALL_RACES.keys()):
class DualDashGraph: """ The DualDashGraph class is the inerface for comparing and highlighting the difference between two graphs. Two Graph class objects should be supplied - such as MST and ALMST graphs. """ def __init__(self, graph_one, graph_two, app_display='default'): """ Initialises the dual graph interface and generates the interface layout. :param graph_one: (Graph) The first graph for the comparison interface. :param graph_two: (Graph) The second graph for the comparison interface. :param app_display: (str) 'default' by default and 'jupyter notebook' for running Dash inside Jupyter Notebook. """ # Dash app styling with Bootstrap if app_display == 'jupyter notebook': self.app = JupyterDash(__name__, external_stylesheets=[dbc.themes.BOOTSTRAP]) else: self.app = Dash(__name__, external_stylesheets=[dbc.themes.BOOTSTRAP]) # Setting input graphs as objest variables cyto.load_extra_layouts() self.graph_one = graph_one self.graph_two = graph_two # Getting a list of tuples with differnet edge connections difference = graph_one.get_difference(graph_two) # Updating the elements needed for the Dash Cytoscape Graph object self.one_components = None self.two_components = None self._update_elements_dual(self.graph_one, difference, 1) self._update_elements_dual(self.graph_two, difference, 2) self.cyto_one = None self.cyto_two = None # Callback functions to allow simultaneous node selection when clicked self.app.callback(Output('cytoscape_two', 'elements'), [Input('cytoscape', 'tapNode')], [State('cytoscape_two', 'elements')])( DualDashGraph._select_other_graph_node) self.app.callback(Output('cytoscape', 'elements'), [Input('cytoscape_two', 'tapNode')], [State('cytoscape', 'elements')])( DualDashGraph._select_other_graph_node) @staticmethod def _select_other_graph_node(data, elements): """ Callback function to select the other graph node when a graph node is selected by setting selected to True. :param data: (Dict) Dictionary of "tapped" or selected node. :param elements: (Dict) Dictionary of elements. :return: (Dict) Returns updates dictionary of elements. """ if data: for element in elements: element['selected'] = ( data['data']['id'] == element.get('data').get('id')) return elements def _generate_comparison_layout(self, graph_one, graph_two): """ Returns and generates a dual comparison layout. :param graph_one: (Graph) The first graph object for the dual interface. :param graph_two: (Graph) Comparison graph object for the dual interface. :return: (html.Div) Returns a Div containing the interface. """ # Set Graph names graph_one_name = type(graph_one).__name__ graph_two_name = type(graph_two).__name__ # Set the cyto graphs self._set_cyto_graph() # Get different edges between two graphs difference = graph_one.get_difference(graph_two) # Layout components padding = {'padding': '10px 10px 10px 10px'} cards = dbc.CardDeck([ dbc.Card([ dbc.CardHeader(graph_one_name), dbc.CardBody(self.cyto_one), ], ), dbc.Card( [dbc.CardHeader(graph_two_name), dbc.CardBody(self.cyto_two)], ) ], style=padding) summary = dbc.Card([ html.H5("Summary", className="card-title"), html.P("{} nodes in each graph and {} different edge(s) per graph." .format(graph_one.get_graph().number_of_nodes(), int(len(difference) / 2)), className="card-text") ], className="w-50", style={ 'margin': '0 auto', 'padding': '10px 10px 10px 10px' }) layout = html.Div([ dbc.Row(dbc.Col(cards, width=12, align='center')), summary, ], style={'padding-bottom': '10px'}) return layout @staticmethod def _get_default_stylesheet(weights): """ Returns the default stylesheet for initialisation. :param weights: (List) A list of weights of the edges. :return: (List) A List of definitions used for Dash styling. """ stylesheet = \ [ { 'selector': 'node', 'style': { 'label': 'data(label)', 'text-valign': 'center', 'background-color': '#4cc9f0', 'font-family': 'sans-serif', 'font-size': '12', 'font-weight': 'bold', 'border-width': 1.5, 'border-color': '#161615', } }, { "selector": 'edge', "style": { 'label': 'data(weight)', "line-color": "#4cc9f0", 'font-size': '8', } }, { "selector": '[weight => 0]', "style": { "width": "mapData(weight, 0, {}, 1, 8)".format(max(weights)), } }, { "selector": '[weight < 0]', "style": { "width": "mapData(weight, 0, {}, 1, 8)".format(min(weights)), } }, { "selector": '.central', "style": { "background-color": "#80b918" } }, { 'selector': ':selected', "style": { "border-width": 2, 'background-color': '#f72585', "border-color": "black", "border-opacity": 1, "opacity": 1, "label": "data(label)", "color": "black", "font-size": 12, 'z-index': 9999 } }, { "selector": '.different', "style": { "line-color": "#f72585", } } ] return stylesheet def _set_cyto_graph(self): """ Updates and sets the two cytoscape graphs using the corresponding components. """ layout = {'name': 'cose-bilkent'} style = { 'width': '100%', 'height': '600px', 'padding': '5px 3px 5px 3px' } self.cyto_one = cyto.Cytoscape( id="cytoscape", layout=layout, style=style, elements=self.one_components[1], stylesheet=DualDashGraph._get_default_stylesheet( self.one_components[0])) self.cyto_two = cyto.Cytoscape( id="cytoscape_two", layout=layout, style=style, elements=self.two_components[1], stylesheet=DualDashGraph._get_default_stylesheet( self.two_components[0])) def _update_elements_dual(self, graph, difference, graph_number): """ Updates the elements needed for the Dash Cytoscape Graph object. :param graph: (Graph) Graph object such as MST or ALMST. :param difference: (List) List of edges where the two graphs differ. :param graph_number: (Int) Graph number to update the correct graph. """ weights = [] elements = [] for node in graph.get_pos(): # If a node is "central", add the central label as a class if graph.get_graph().degree(node) >= 5: elements.append({ 'data': { 'id': node, 'label': node }, 'selectable': 'true', 'classes': 'central' }) else: elements.append({ 'data': { 'id': node, 'label': node }, 'selectable': 'true', }) for node1, node2, weight in graph.get_graph().edges(data=True): element = { 'data': { 'source': node1, 'target': node2, 'weight': round(weight['weight'], 4) } } # If the edge is a "different" edge, label with class "different" to highlight this edge if (node1, node2) in difference: element = { 'data': { 'source': node1, 'target': node2, 'weight': round(weight['weight'], 4) }, 'classes': 'different' } weights.append(round(weight['weight'], 4)) elements.append(element) # Update correct graph components if graph_number == 1: self.one_components = (weights, elements) if graph_number == 2: self.two_components = (weights, elements) def get_server(self): """ Returns the comparison interface server :return: (Dash) Returns the Dash app object, which can be run using run_server. Returns a Jupyter Dash object if DashGraph has been initialised for Jupyter Notebook. """ # Create an app from a comparison layout self.app.layout = self._generate_comparison_layout( self.graph_one, self.graph_two) # Return the app return self.app
class DashGraph: """ This DashGraph class creates a server for Dash cytoscape visualisations. """ def __init__(self, input_graph, app_display='default'): """ Initialises the DashGraph object from the Graph class object. Dash creates a mini Flask server to visualise the graphs. :param app_display: (str) 'default' by default and 'jupyter notebook' for running Dash inside Jupyter Notebook. :param input_graph: (Graph) Graph class from graph.py. """ self.graph = None # Dash app styling with Bootstrap if app_display == 'jupyter notebook': self.app = JupyterDash(__name__, external_stylesheets=[dbc.themes.BOOTSTRAP]) else: self.app = Dash(__name__, external_stylesheets=[dbc.themes.BOOTSTRAP]) # Graph class object self.graph = input_graph # The dictionary of the nodes coordinates self.pos = self.graph.get_pos() # Colours of nodes self.colour_groups = {} # If colours have been assigned in Graph class, add styling if self.graph.get_node_colours(): colour_map = self.graph.get_node_colours() self._assign_colours_to_groups(list(colour_map.keys())) self.weights = [] self.elements = [] self._update_elements() # Load the different graph layouts cyto.load_extra_layouts() self.layout_options = ['cose-bilkent', 'cola', 'spread'] self.statistics = [ 'graph_summary', 'average_degree_connectivity', 'average_neighbor_degree', 'betweenness_centrality' ] # Load default stylesheet self.stylesheet = None self.stylesheet = self._get_default_stylesheet() # Append stylesheet for colour and size # If sizes have been set in the Graph class self._style_colours() if self.graph.get_node_sizes(): self._assign_sizes() self.cyto_graph = None # Callback functions to hook frontend elements to functions self.app.callback(Output('cytoscape', 'layout'), [Input('dropdown-layout', 'value')])( DashGraph._update_cytoscape_layout) self.app.callback(Output('json-output', 'children'), [Input('dropdown-stat', 'value')])( self._update_stat_json) self.app.callback(Output('cytoscape', 'elements'), [Input('rounding_decimals', 'value')])( self._round_decimals) def _set_cyto_graph(self): """ Sets the cytoscape graph elements. """ self.cyto_graph = cyto.Cytoscape( id="cytoscape", layout={'name': self.layout_options[0]}, style={ 'width': '100%', 'height': '600px', 'padding': '5px 3px 5px 3px' }, elements=self.elements, stylesheet=self.stylesheet) def _get_node_group(self, node_name): """ Returns the industry or sector name for a given node name. :param node_name: (str) Name of a given node in the graph. :return: (str) Name of industry that the node is in or "default" for nodes which haven't been assigned a group. """ node_colour_map = self.graph.get_node_colours() for key, val in node_colour_map.items(): if node_name in val: return key return "default" def _get_node_size(self, index): """ Returns the node size for given node index if the node sizes have been set. :param index: (int) The index of the node. :return: (float) Returns size of node set, 0 if it has not been set. """ if self.graph.get_node_sizes(): return self.graph.get_node_sizes()[index] return 0 def _update_elements(self, dps=4): """ Updates the elements needed for the Dash Cytoscape Graph object. :param dps: (int) Decimal places to round the edge values. """ i = 0 self.weights = [] self.elements = [] for node in self.pos: self.elements.append({ 'data': { 'id': node, 'label': node, 'colour_group': self._get_node_group(node), 'size': self._get_node_size(i) }, 'selectable': 'true', }) i += 1 for node1, node2, weight in self.graph.get_graph().edges(data=True): self.weights.append(round(weight['weight'], dps)) self.elements.append({ 'data': { 'source': node1, 'target': node2, 'weight': round(weight['weight'], dps) } }) def _generate_layout(self): """ Generates the layout for cytoscape. :return: (dbc.Container) Returns Dash Bootstrap Component Container containing the layout of UI. """ graph_type = type(self.graph).__name__ self._set_cyto_graph() layout_input = [ html.H1("{} from {} matrix".format(graph_type, self.graph.get_matrix_type())), html.Hr(), dbc.Row( [ dbc.Col(self._get_default_controls(), md=4), dbc.Col(self.cyto_graph, md=8), ], align="center", ) ] if self.colour_groups: layout_input.append(self._get_toast()) layout = dbc.Container( layout_input, fluid=True, ) return layout def _assign_colours_to_groups(self, groups): """ Assigns the colours to industry or sector groups by creating a dictionary of group name to colour. :param groups: (List) List of industry groups as strings. """ # List of colours selected to match with industry groups colours = [ "#d0b7d5", "#a0b3dc", "#90e190", "#9bd8de", "#eaa2a2", "#f6c384", "#dad4a2", '#ff52a8', '#ffd1e8', '#bd66ff', '#6666ff', '#66ffff', '#00e600', '#fff957', '#ffc966', '#ff8833', '#ff6666', '#C0C0C0', '#008080' ] # Random colours are generated if industry groups added exceeds 19 while len(groups) > len(colours): random_number = random.randint(0, 16777215) hex_number = str(hex(random_number)) hex_number = '#' + hex_number[2:] colours.append(hex_number) # Create and add to the colour map colour_map = {} for i, item in enumerate(groups): colour_map[item] = colours[i].capitalize() self.colour_groups = colour_map def _style_colours(self): """ Appends the colour styling to stylesheet for the different groups. """ if self.colour_groups: keys = list(self.colour_groups.keys()) for item in keys: new_colour = { "selector": "node[colour_group=\"{}\"]".format(item), "style": { 'background-color': '{}'.format(self.colour_groups[item]), } } self.stylesheet.append(new_colour) def _assign_sizes(self): """ Assigns the node sizing by appending to the stylesheet. """ sizes = self.graph.get_node_sizes() max_size = max(sizes) min_size = min(sizes) new_sizes = { 'selector': 'node', 'style': { "width": "mapData(size, {min}, {max}, 25, 250)".format(min=min_size, max=max_size), "height": "mapData(size, {min}, {max}, 25, 250)".format(min=min_size, max=max_size), } } self.stylesheet.append(new_sizes) def get_server(self): """ Returns a small Flask server. :return: (Dash) Returns the Dash app object, which can be run using run_server. Returns a Jupyter Dash object if DashGraph has been initialised for Jupyter Notebook. """ self.app.layout = self._generate_layout() return self.app @staticmethod def _update_cytoscape_layout(layout): """ Callback function for updating the cytoscape layout. The useful layouts for MST have been included as options (cola, cose-bilkent, spread). :return: (Dict) Dictionary of the key 'name' to the desired layout (e.g. cola, spread). """ return {'name': layout} def _update_stat_json(self, stat_name): """ Callback function for updating the statistic shown. :param stat_name: (str) Name of the statistic to display (e.g. graph_summary). :return: (json) Json of the graph information depending on chosen statistic. """ switcher = { "graph_summary": self.get_graph_summary(), "average_degree_connectivity": nx.average_degree_connectivity(self.graph.get_graph()), "average_neighbor_degree": nx.average_neighbor_degree(self.graph.get_graph()), "betweenness_centrality": nx.betweenness_centrality(self.graph.get_graph()), } if type(self.graph).__name__ == "PMFG": switcher["disparity_measure"] = self.graph.get_disparity_measure() return json.dumps(switcher.get(stat_name), indent=2) def get_graph_summary(self): """ Returns the Graph Summary statistics. The following statistics are included - the number of nodes and edges, smallest and largest edge, average node connectivity, normalised tree length and the average shortest path. :return: (Dict) Dictionary of graph summary statistics. """ summary = { "nodes": len(self.pos), "edges": self.graph.get_graph().number_of_edges(), "smallest_edge": min(self.weights), "largest_edge": max(self.weights), "average_node_connectivity": nx.average_node_connectivity(self.graph.get_graph()), "normalised_tree_length": (sum(self.weights) / (len(self.weights))), "average_shortest_path": nx.average_shortest_path_length(self.graph.get_graph()) } return summary def _round_decimals(self, dps): """ Callback function for updating decimal places. Updates the elements to modify the rounding of edge values. :param dps: (int) Number of decimals places to round to. :return: (List) Returns the list of elements used to define graph. """ if dps: self._update_elements(dps) return self.elements def _get_default_stylesheet(self): """ Returns the default stylesheet for initialisation. :return: (List) A List of definitions used for Dash styling. """ stylesheet = \ [ { 'selector': 'node', 'style': { 'label': 'data(label)', 'text-valign': 'center', 'background-color': '#65afff', 'color': '', 'font-family': 'sans-serif', 'font-size': '12', 'font-weight': 'bold', 'border-width': 1.5, 'border-color': '#161615', } }, { "selector": 'edge', "style": { 'label': 'data(weight)', "line-color": "#a3d5ff", 'font-size': '8', } }, { "selector": '[weight => 0]', "style": { "width": "mapData(weight, 0, {}, 1, 8)".format(max(self.weights)), } }, { "selector": '[weight < 0]', "style": { "width": "mapData(weight, 0, {}, 1, 8)".format(min(self.weights)), } } ] return stylesheet def _get_toast(self): """ Toast is the floating colour legend to display when industry groups have been added. This method returns the toast component with the styled colour legend. :return: (html.Div) Returns Div containing colour legend. """ list_elements = [] for industry, colour in self.colour_groups.items(): span_styling = \ { "border": "1px solid #ccc", "background-color": colour, "float": "left", "width": "12px", "height": "12px", "margin-right": "5px" } children = [industry.title(), html.Span(style=span_styling)] list_elements.append(html.Li(children)) toast = html.Div([ dbc.Toast( html.Ul(list_elements, style={ "list-style": "None", "padding-left": 0 }), id="positioned-toast", header="Industry Groups", dismissable=True, # stuck on bottom right corner style={ "position": "fixed", "bottom": 36, "right": 10, "width": 350 }, ), ]) return toast def _get_default_controls(self): """ Returns the default controls for initialisation. :return: (dbc.Card) Dash Bootstrap Component Card which defines the side panel. """ controls = dbc.Card( [ html.Div([ dbc.FormGroup([ dbc.Label("Graph Layout"), dcc.Dropdown( id="dropdown-layout", options=[{ "label": col, "value": col } for col in self.layout_options], value=self.layout_options[0], clearable=False, ), ]), dbc.FormGroup([ dbc.Label("Statistic Type"), dcc.Dropdown( id="dropdown-stat", options=[{ "label": col, "value": col } for col in self.statistics], value="graph_summary", clearable=False, ), ]), html.Pre(id='json-output', style={ 'overflow-y': 'scroll', 'height': '100px', 'border': 'thin lightgrey solid' }), dbc.FormGroup([ dbc.Label("Decimal Places"), dbc.Input(id="rounding_decimals", type="number", value=4, min=1), ]), ]), dbc.CardBody(html.Div(id="card-content", className="card-text")), ], body=True, ) return controls
def test_cblp001_radio_buttons_callbacks_generating_children(dash_duo): TIMEOUT = 2 with open(os.path.join(os.path.dirname(__file__), "state_path.json")) as fp: EXPECTED_PATHS = json.load(fp) app = Dash(__name__) app.layout = html.Div( [ dcc.RadioItems( options=[ {"label": "Chapter 1", "value": "chapter1"}, {"label": "Chapter 2", "value": "chapter2"}, {"label": "Chapter 3", "value": "chapter3"}, {"label": "Chapter 4", "value": "chapter4"}, {"label": "Chapter 5", "value": "chapter5"}, ], value="chapter1", id="toc", ), html.Div(id="body"), ] ) for script in dcc._js_dist: app.scripts.append_script(script) chapters = { "chapter1": html.Div( [ html.H1("Chapter 1", id="chapter1-header"), dcc.Dropdown( options=[{"label": i, "value": i} for i in ["NYC", "MTL", "SF"]], value="NYC", id="chapter1-controls", ), html.Label(id="chapter1-label"), dcc.Graph(id="chapter1-graph"), ] ), # Chapter 2 has the some of the same components in the same order # as Chapter 1. This means that they won't get remounted # unless they set their own keys are differently. # Switching back and forth between 1 and 2 implicitly # tests how components update when they aren't remounted. "chapter2": html.Div( [ html.H1("Chapter 2", id="chapter2-header"), dcc.RadioItems( options=[{"label": i, "value": i} for i in ["USA", "Canada"]], value="USA", id="chapter2-controls", ), html.Label(id="chapter2-label"), dcc.Graph(id="chapter2-graph"), ] ), # Chapter 3 has a different layout and so the components # should get rewritten "chapter3": [ html.Div( html.Div( [ html.H3("Chapter 3", id="chapter3-header"), html.Label(id="chapter3-label"), dcc.Graph(id="chapter3-graph"), dcc.RadioItems( options=[ {"label": i, "value": i} for i in ["Summer", "Winter"] ], value="Winter", id="chapter3-controls", ), ] ) ) ], # Chapter 4 doesn't have an object to recursively traverse "chapter4": "Just a string", } call_counts = { "body": Value("i", 0), "chapter1-graph": Value("i", 0), "chapter1-label": Value("i", 0), "chapter2-graph": Value("i", 0), "chapter2-label": Value("i", 0), "chapter3-graph": Value("i", 0), "chapter3-label": Value("i", 0), } @app.callback(Output("body", "children"), [Input("toc", "value")]) def display_chapter(toc_value): call_counts["body"].value += 1 return chapters[toc_value] app.config.suppress_callback_exceptions = True def generate_graph_callback(counterId): def callback(value): call_counts[counterId].value += 1 return { "data": [ { "x": ["Call Counter for: {}".format(counterId)], "y": [call_counts[counterId].value], "type": "bar", } ], "layout": { "title": value, "width": 500, "height": 400, "margin": {"autoexpand": False}, }, } return callback def generate_label_callback(id_): def update_label(value): call_counts[id_].value += 1 return value return update_label for chapter in ["chapter1", "chapter2", "chapter3"]: app.callback( Output("{}-graph".format(chapter), "figure"), [Input("{}-controls".format(chapter), "value")], )(generate_graph_callback("{}-graph".format(chapter))) app.callback( Output("{}-label".format(chapter), "children"), [Input("{}-controls".format(chapter), "value")], )(generate_label_callback("{}-label".format(chapter))) dash_duo.start_server(app) def check_chapter(chapter): dash_duo.wait_for_element("#{}-graph:not(.dash-graph--pending)".format(chapter)) for key in dash_duo.redux_state_paths["strs"]: assert dash_duo.find_elements( "#{}".format(key) ), "each element should exist in the dom" value = ( chapters[chapter][0]["{}-controls".format(chapter)].value if chapter == "chapter3" else chapters[chapter]["{}-controls".format(chapter)].value ) # check the actual values dash_duo.wait_for_text_to_equal("#{}-label".format(chapter), value) wait.until( lambda: ( dash_duo.driver.execute_script( 'return document.querySelector("' + "#{}-graph:not(.dash-graph--pending) .js-plotly-plot".format( chapter ) + '").layout.title.text' ) == value ), TIMEOUT, ) assert dash_duo.redux_state_rqs == [], "pendingCallbacks is empty" def check_call_counts(chapters, count): for chapter in chapters: assert call_counts[chapter + "-graph"].value == count assert call_counts[chapter + "-label"].value == count wait.until(lambda: call_counts["body"].value == 1, TIMEOUT) wait.until(lambda: call_counts["chapter1-graph"].value == 1, TIMEOUT) wait.until(lambda: call_counts["chapter1-label"].value == 1, TIMEOUT) check_call_counts(("chapter2", "chapter3"), 0) assert dash_duo.redux_state_paths == EXPECTED_PATHS["chapter1"] check_chapter("chapter1") dash_duo.percy_snapshot(name="chapter-1") dash_duo.find_elements('input[type="radio"]')[1].click() # switch chapters wait.until(lambda: call_counts["body"].value == 2, TIMEOUT) wait.until(lambda: call_counts["chapter2-graph"].value == 1, TIMEOUT) wait.until(lambda: call_counts["chapter2-label"].value == 1, TIMEOUT) check_call_counts(("chapter1",), 1) assert dash_duo.redux_state_paths == EXPECTED_PATHS["chapter2"] check_chapter("chapter2") dash_duo.percy_snapshot(name="chapter-2") # switch to 3 dash_duo.find_elements('input[type="radio"]')[2].click() wait.until(lambda: call_counts["body"].value == 3, TIMEOUT) wait.until(lambda: call_counts["chapter3-graph"].value == 1, TIMEOUT) wait.until(lambda: call_counts["chapter3-label"].value == 1, TIMEOUT) check_call_counts(("chapter2", "chapter1"), 1) assert dash_duo.redux_state_paths == EXPECTED_PATHS["chapter3"] check_chapter("chapter3") dash_duo.percy_snapshot(name="chapter-3") dash_duo.find_elements('input[type="radio"]')[3].click() # switch to 4 dash_duo.wait_for_text_to_equal("#body", "Just a string") dash_duo.percy_snapshot(name="chapter-4") paths = dash_duo.redux_state_paths assert paths["objs"] == {} for key in paths["strs"]: assert dash_duo.find_elements( "#{}".format(key) ), "each element should exist in the dom" assert paths["strs"] == { "toc": ["props", "children", 0], "body": ["props", "children", 1], } dash_duo.find_elements('input[type="radio"]')[0].click() wait.until( lambda: dash_duo.redux_state_paths == EXPECTED_PATHS["chapter1"], TIMEOUT ) check_chapter("chapter1") dash_duo.percy_snapshot(name="chapter-1-again")
def add_slide1_callbacks(dash: Dash) -> None: """Add routing callback""" dash.callback(Output("slide1-tab-content", "children"), [Input("slide1-tabs", "active_tab")])(update_tab)
def set_callbacks(self, app: Dash) -> None: @app.callback( [ Output(self.uuid("map"), "layers"), Output(self.uuid("map2"), "layers"), Output(self.uuid("map3"), "layers"), Output(self.uuid("map3-label"), "children"), ], [ Input(self.selector.storage_id, "data"), Input(self.uuid("ensemble"), "value"), Input(self.uuid("realization"), "value"), Input(self.selector2.storage_id, "data"), Input(self.uuid("ensemble2"), "value"), Input(self.uuid("realization2"), "value"), Input(self.uuid("calculation"), "value"), Input(self.uuid("attribute-settings"), "data"), Input(self.uuid("truncate-diff-min"), "value"), Input(self.uuid("truncate-diff-max"), "value"), Input(self.uuid("map"), "switch"), Input(self.uuid("map2"), "switch"), Input(self.uuid("map3"), "switch"), ], ) # pylint: disable=too-many-arguments, too-many-locals def _set_base_layer( stored_selector_data: str, ensemble: str, real: str, stored_selector2_data: str, ensemble2: str, real2: str, calculation: str, stored_attribute_settings: str, diff_min: Union[int, float, None], diff_max: Union[int, float, None], hillshade: dict, hillshade2: dict, hillshade3: dict, ) -> Tuple[List[dict], List[dict], List[dict], str]: ctx = callback_context.triggered if not ctx or not stored_selector_data or not stored_selector2_data: raise PreventUpdate # TODO(Sigurd) # These two are presumably of type dict, but the type depends on the actual python # objects that get serialized inside SurfaceSelector. # Should deserialization and validation be delegated to SurfaceSelector? # Note that according to the doc, it seems that dcc.Store actualy does the # serialization/deserialization for us! # Should be refactored data: dict = json.loads(stored_selector_data) data2: dict = json.loads(stored_selector2_data) if not isinstance(data, dict) or not isinstance(data2, dict): raise TypeError("Selector data payload must be of type dict") attribute_settings: dict = json.loads(stored_attribute_settings) if not isinstance(attribute_settings, dict): raise TypeError("Expected stored attribute_settings to be of type dict") if real in ["Mean", "StdDev", "Min", "Max"]: surface = self._surface_ensemble_set_model[ ensemble ].calculate_statistical_surface(**data, calculation=real) else: surface = self._surface_ensemble_set_model[ ensemble ].get_realization_surface(**data, realization=int(real)) if real2 in ["Mean", "StdDev", "Min", "Max"]: surface2 = self._surface_ensemble_set_model[ ensemble2 ].calculate_statistical_surface(**data2, calculation=real2) else: surface2 = self._surface_ensemble_set_model[ ensemble2 ].get_realization_surface(**data2, realization=int(real2)) surface_layers: List[dict] = [ SurfaceLeafletModel( surface, name="surface", colors=attribute_settings.get(data["attribute"], {}).get("color"), apply_shading=hillshade.get("value", False), clip_min=attribute_settings.get(data["attribute"], {}).get( "min", None ), clip_max=attribute_settings.get(data["attribute"], {}).get( "max", None ), unit=attribute_settings.get(data["attribute"], {}).get("unit", " "), ).layer ] surface_layers2: List[dict] = [ SurfaceLeafletModel( surface2, name="surface2", colors=attribute_settings.get(data2["attribute"], {}).get("color"), apply_shading=hillshade2.get("value", False), clip_min=attribute_settings.get(data2["attribute"], {}).get( "min", None ), clip_max=attribute_settings.get(data2["attribute"], {}).get( "max", None ), unit=attribute_settings.get(data2["attribute"], {}).get( "unit", " " ), ).layer ] try: surface3 = calculate_surface_difference(surface, surface2, calculation) if diff_min is not None: surface3.values[surface3.values <= diff_min] = diff_min if diff_max is not None: surface3.values[surface3.values >= diff_max] = diff_max diff_layers: List[dict] = [] diff_layers.append( SurfaceLeafletModel( surface3, name="surface3", colors=attribute_settings.get(data["attribute"], {}).get( "color" ), apply_shading=hillshade3.get("value", False), ).layer ) error_label = "" except ValueError: diff_layers = [] error_label = ( "Cannot calculate because the surfaces have different geometries" ) if self.well_layer: surface_layers.append(self.well_layer) surface_layers2.append(self.well_layer) diff_layers.append(self.well_layer) return (surface_layers, surface_layers2, diff_layers, error_label) def _update_from_btn( _n_prev: int, _n_next: int, current_value: str, options: List[dict] ) -> str: """Updates dropdown value if previous/next btn is clicked""" option_values: List[str] = [opt["value"] for opt in options] ctx = callback_context.triggered if not ctx or current_value is None: raise PreventUpdate if not ctx[0]["value"]: return current_value callback = ctx[0]["prop_id"] if "-prev" in callback: return prev_value(current_value, option_values) if "-next" in callback: return next_value(current_value, option_values) return current_value for btn_name in ["ensemble", "realization", "ensemble2", "realization2"]: app.callback( Output(self.uuid(f"{btn_name}"), "value"), [ Input(self.uuid(f"{btn_name}-prev"), "n_clicks"), Input(self.uuid(f"{btn_name}-next"), "n_clicks"), ], [ State(self.uuid(f"{btn_name}"), "value"), State(self.uuid(f"{btn_name}"), "options"), ], )(_update_from_btn)
def activate_all(dash: Dash) -> NoReturn: """Активирует все контролерры для указанного Dash-приложения.""" for controller in CONTROLLERS: kwargs = controller.dict() func = kwargs.pop("func") dash.callback(**kwargs)(func)