diff --git a/TODO.md b/TODO.md deleted file mode 100644 index 5d9a4ac4..00000000 --- a/TODO.md +++ /dev/null @@ -1,37 +0,0 @@ -could you make a Dash app that is similar to our editor in http://plot.ly/create/? - -basically: -- The Graph on the left side of the screen -- On the right side of the screen, a control panel -- In that control panel, enumerate through: -- the `style` properties here: http://js.cytoscape.org/#style -- `layout` properties here: http://js.cytoscape.org/#layouts -- `instantiation` properies here: http://js.cytoscape.org/#core/initialisation -- the `elements` properties here: http://js.cytoscape.org/#notation/elements-json - -it’ll eventually serve as our documentation -sort of like “interactive documentation” - -and then we can even have an output tab that prints out the input arguments as JSON - -we could have them as JSON imports in some type of top-level dropdown -similar to here: https://plotly.github.io/react-chart-editor/ -(the “Select mock” dropdown at the bottom of the page) - -so, i’d start with just a few properties from each section (`layouts`, `style`, `elements`) and create a simple app and put that in a PR -and then I can review your structure -and then we can go at it and fill the rest of the stuff out - -we can also use that app as our test suite - we’ll select different options and take screenshots - -let’s first start to explore the API and make sure that editing these things work -and by exploring the API, I mean doing so through a sample app -i.e. spend 3-5 days on that -and then once that is de-risked, we’ll start looking into the event system - - - -## List of Styles not implemented -* Visibility -* Labels/Wrapping text -* Events \ No newline at end of file diff --git a/demos/usage-phylogeny.py b/demos/usage-phylogeny.py index c44313a0..8e07eb14 100644 --- a/demos/usage-phylogeny.py +++ b/demos/usage-phylogeny.py @@ -1,3 +1,5 @@ +import math + import dash_cytoscape import dash from dash.dependencies import Input, Output @@ -7,52 +9,122 @@ try: from Bio import Phylo except ModuleNotFoundError as e: - print(e, "Please make sure you have biopython installed correctly before running this example.") + print(e, + "Please make sure biopython is installed correctly before running this example.") exit(1) -def generate_elements(tree): - elements = [] - - def _add_to_elements(clade, clade_id): +def generate_elements(tree, xlen=30, ylen=30, grabbable=False): + def get_col_positions(tree, column_width=80): + taxa = tree.get_terminals() + + # Some constants for the drawing calculations + max_label_width = max(len(str(taxon)) for taxon in taxa) + drawing_width = column_width - max_label_width - 1 + + """Create a mapping of each clade to its column position.""" + depths = tree.depths() + # If there are no branch lengths, assume unit branch lengths + if not max(depths.values()): + depths = tree.depths(unit_branch_lengths=True) + # Potential drawing overflow due to rounding -- 1 char per tree layer + fudge_margin = int(math.ceil(math.log(len(taxa), 2))) + cols_per_branch_unit = ((drawing_width - fudge_margin) / + float(max(depths.values()))) + return dict((clade, int(blen * cols_per_branch_unit + 1.0)) + for clade, blen in depths.items()) + + def get_row_positions(tree): + taxa = tree.get_terminals() + positions = dict((taxon, 2 * idx) for idx, taxon in enumerate(taxa)) + + def calc_row(clade): + for subclade in clade: + if subclade not in positions: + calc_row(subclade) + positions[clade] = ((positions[clade.clades[0]] + + positions[clade.clades[-1]]) // 2) + + calc_row(tree.root) + return positions + + def add_to_elements(clade, clade_id): children = clade.clades - cy_source = {"data": {"id": clade_id}, 'classes': 'nonterminal'} - elements.append(cy_source) + pos_x = col_positions[clade] * xlen + pos_y = row_positions[clade] * ylen + + cy_source = { + "data": {"id": clade_id}, + 'position': {'x': pos_x, 'y': pos_y}, + 'classes': 'nonterminal', + 'grabbable': grabbable + } + nodes.append(cy_source) if clade.is_terminal(): cy_source['data']['name'] = clade.name cy_source['classes'] = 'terminal' - for n, child in enumerate(children, 1): - child_id = len(elements) + n - - cy_edge = {'data': { - 'source': clade_id, - 'target': child_id, - 'length': clade.branch_length - }} + for n, child in enumerate(children): + # The "support" node is on the same column as the parent clade, + # and on the same row as the child clade. It is used to create the + # 90 degree angle between the parent and the children. + # Edge config: parent -> support -> child + + support_id = clade_id + 's' + str(n) + child_id = clade_id + 'c' + str(n) + pos_y_child = row_positions[child] * ylen + + cy_support_node = { + 'data': {'id': support_id}, + 'position': {'x': pos_x, 'y': pos_y_child}, + 'grabbable': grabbable, + 'classes': 'support' + } + + cy_support_edge = { + 'data': { + 'source': clade_id, + 'target': support_id, + 'sourceCladeId': clade_id + }, + } + + cy_edge = { + 'data': { + 'source': support_id, + 'target': child_id, + 'length': clade.branch_length, + 'sourceCladeId': clade_id + }, + } if clade.confidence and clade.confidence.value: cy_source['data']['confidence'] = clade.confidence.value - elements.extend([cy_edge]) + nodes.append(cy_support_node) + edges.extend([cy_support_edge, cy_edge]) - _add_to_elements(child, child_id) + add_to_elements(child, child_id) - _add_to_elements(tree.clade, 0) + col_positions = get_col_positions(tree) + row_positions = get_row_positions(tree) - return elements + nodes = [] + edges = [] + + add_to_elements(tree.clade, 'r') + + return nodes, edges # Define elements, stylesheet and layout tree = Phylo.read('data/apaf.xml', 'phyloxml') -elements = generate_elements(tree) +nodes, edges = generate_elements(tree) +elements = nodes + edges -layout = { - 'name': 'breadthfirst', - 'directed': True -} +layout = {'name': 'preset'} stylesheet = [ { @@ -64,36 +136,36 @@ def _add_to_elements(clade, clade_id): "text-valign": "top", } }, + { + 'selector': '.support', + 'style': {'background-opacity': 0} + }, { 'selector': 'edge', 'style': { - "source-endpoint": "outside-to-node", + "source-endpoint": "inside-to-node", + "target-endpoint": "inside-to-node", } }, { 'selector': '.terminal', 'style': { 'label': 'data(name)', - "shape": "roundrectangle", - "width": 115, - "height": 25, + 'width': 10, + 'height': 10, "text-valign": "center", - 'background-color': 'white', - "border-width": 1.5, - "border-style": "solid", - "border-opacity": 1, + "text-halign": "right", + 'background-color': '#222222' } } ] - # Start the app app = dash.Dash(__name__) app.scripts.config.serve_locally = True app.css.config.serve_locally = True - app.layout = html.Div([ dash_cytoscape.Cytoscape( id='cytoscape', @@ -108,5 +180,26 @@ def _add_to_elements(clade, clade_id): ]) +@app.callback(Output('cytoscape', 'stylesheet'), + [Input('cytoscape', 'mouseoverEdgeData')]) +def color_children(edgeData): + if not edgeData: + return stylesheet + + if 's' in edgeData['source']: + val = edgeData['source'].split('s')[0] + else: + val = edgeData['source'] + + children_style = [{ + 'selector': f'edge[source *= "{val}"]', + 'style': { + 'line-color': 'blue' + } + }] + + return stylesheet + children_style + + if __name__ == '__main__': app.run_server(debug=True)