From c78451f1bd0487109591cf81740c1d5a10f3d2ae Mon Sep 17 00:00:00 2001 From: Dannon Baker Date: Tue, 16 Feb 2016 15:59:44 -0500 Subject: [PATCH 1/7] Initial swap from svgfig to svgwrite, more or less drop-in replacement of the same rendering approach and look/feel. --- .../webapps/galaxy/controllers/workflow.py | 11 +-- lib/galaxy/workflow/render.py | 73 ++++++++++--------- 2 files changed, 43 insertions(+), 41 deletions(-) diff --git a/lib/galaxy/webapps/galaxy/controllers/workflow.py b/lib/galaxy/webapps/galaxy/controllers/workflow.py index 63cf2dc511e..dabc0ab3d48 100644 --- a/lib/galaxy/webapps/galaxy/controllers/workflow.py +++ b/lib/galaxy/webapps/galaxy/controllers/workflow.py @@ -514,7 +514,7 @@ class WorkflowController( BaseUIController, SharableMixin, UsesStoredWorkflowMix def gen_image( self, trans, id ): stored = self.get_stored_workflow( trans, id, check_ownership=True ) trans.response.set_content_type("image/svg+xml") - return self._workflow_to_svg_canvas( trans, stored ).standalone_xml() + return self._workflow_to_svg_canvas( trans, stored ) @web.expose @web.require_login( "use Galaxy workflows" ) @@ -750,7 +750,7 @@ class WorkflowController( BaseUIController, SharableMixin, UsesStoredWorkflowMix workflow_name=workflow_dict['name'], workflow_description=workflow_dict['annotation'], workflow_content=workflow_content, - workflow_svg=self._workflow_to_svg_canvas( trans, stored ).standalone_xml() + workflow_svg=self._workflow_to_svg_canvas( trans, stored ).tostring() ) # strip() b/c myExperiment XML parser doesn't allow white space before XML; utf-8 handles unicode characters. request = unicodify( request_raw.strip(), 'utf-8' ) @@ -1291,7 +1291,6 @@ class WorkflowController( BaseUIController, SharableMixin, UsesStoredWorkflowMix def _workflow_to_svg_canvas( self, trans, stored ): workflow = stored.latest_workflow workflow_canvas = WorkflowCanvas() - canvas = workflow_canvas.canvas for step in workflow.steps: # Load from database representation module = module_factory.from_workflow_step( trans, step ) @@ -1304,10 +1303,8 @@ class WorkflowController( BaseUIController, SharableMixin, UsesStoredWorkflowMix module_data_inputs, module_data_outputs, ) - - workflow_canvas.add_steps( ) - workflow_canvas.finish( ) - return canvas + workflow_canvas.add_steps() + return workflow_canvas.finish() def _build_workflow_on_str(instance_ds_names): diff --git a/lib/galaxy/workflow/render.py b/lib/galaxy/workflow/render.py index 52f3db8c406..2376600c9e0 100644 --- a/lib/galaxy/workflow/render.py +++ b/lib/galaxy/workflow/render.py @@ -1,4 +1,4 @@ -import svgfig +import svgwrite MARGIN = 5 LINE_SPACING = 15 @@ -7,11 +7,10 @@ LINE_SPACING = 15 class WorkflowCanvas( object ): def __init__( self ): - self.canvas = svgfig.canvas( style="stroke:black; fill:none; stroke-width:1px; stroke-linejoin:round; text-anchor:left" ) - self.text = svgfig.SVG( "g" ) - self.connectors = svgfig.SVG( "g" ) - self.boxes = svgfig.SVG( "g" ) - svgfig.Text.defaults[ "font-size" ] = "10px" + self.canvas = svgwrite.Drawing(profile='full') + self.connectors = [] + self.boxes = [] + self.text = [] self.in_pos = {} self.out_pos = {} self.widths = {} @@ -21,34 +20,35 @@ class WorkflowCanvas( object ): self.data = [] def finish( self ): - max_x, max_y, max_width = self.max_x, self.max_y, self.max_width - canvas = self.canvas - canvas.append( self.connectors ) - canvas.append( self.boxes ) - canvas.append( self.text ) - width, height = ( max_x + max_width + 50 ), max_y + 300 - canvas[ 'width' ] = "%spx" % width - canvas[ 'height' ] = "%spx" % height - canvas[ 'viewBox' ] = "0 0 %s %s" % ( width, height ) + # max_x, max_y, max_width = self.max_x, self.max_y, self.max_width + for box in self.boxes: + self.canvas.add(box) + for connector in self.connectors: + self.canvas.add(connector) + for text in self.text: + self.canvas.add(text) + # width, height = ( max_x + max_width + 50 ), max_y + 300 + # TODO set width/height? OR flex is OK? + return self.canvas def add_boxes( self, step_dict, width, name_fill ): x, y = step_dict[ 'position' ][ 'left' ], step_dict[ 'position' ][ 'top' ] - self.boxes.append( svgfig.Rect( x - MARGIN, y, x + width - MARGIN, y + 30, fill=name_fill ).SVG() ) + self.boxes.append(svgwrite.shapes.Rect((x - MARGIN, y), (width, 30), fill=name_fill, stroke='#000000')) box_height = ( len( step_dict[ 'data_inputs' ] ) + len( step_dict[ 'data_outputs' ] ) ) * LINE_SPACING + MARGIN # Draw separator line. if len( step_dict[ 'data_inputs' ] ) > 0: box_height += 15 sep_y = y + len( step_dict[ 'data_inputs' ] ) * LINE_SPACING + 40 - self.text.append( svgfig.Line( x - MARGIN, sep_y, x + width - MARGIN, sep_y ).SVG() ) + self.text.append( svgwrite.shapes.Line( (x - MARGIN, sep_y), (x + width - MARGIN, sep_y ), stroke=svgwrite.rgb(0, 0, 0)) ) # Define an input/output box. - self.boxes.append( svgfig.Rect( x - MARGIN, y + 30, x + width - MARGIN, y + 30 + box_height, fill="#ffffff" ).SVG() ) + self.boxes.append( svgwrite.shapes.Rect( (x - MARGIN, y + 30), (width, box_height), fill="#ffffff", stroke=svgwrite.rgb(0, 0, 0) )) def add_text( self, module_data_inputs, module_data_outputs, step, module_name ): left, top = step.position[ 'left' ], step.position[ 'top' ] x, y = left, top order_index = step.order_index max_len = len( module_name ) * 1.5 - self.text.append( svgfig.Text( x, y + 20, module_name, **{ "font-size": "14px" } ).SVG() ) + self.text.append(svgwrite.text.Text(module_name, (x, y + 20))) y += 45 count = 0 in_pos = self.in_pos @@ -58,7 +58,7 @@ class WorkflowCanvas( object ): if order_index not in in_pos: in_pos[ order_index ] = {} in_pos[ order_index ][ di[ 'name' ] ] = ( x, cur_y ) - self.text.append( svgfig.Text( x, cur_y, di[ 'label' ] ).SVG() ) + self.text.append( svgwrite.text.Text( di[ 'label' ], (x, cur_y) ) ) count += 1 max_len = max( max_len, len( di[ 'label' ] ) ) if len( module_data_inputs ) > 0: @@ -68,7 +68,7 @@ class WorkflowCanvas( object ): if order_index not in out_pos: out_pos[ order_index ] = {} out_pos[ order_index ][ do[ 'name' ] ] = ( x, cur_y ) - self.text.append( svgfig.Text( x, cur_y, do[ 'name' ] ).SVG() ) + self.text.append( svgwrite.text.Text( do[ 'name' ], (x, cur_y) )) count += 1 max_len = max( max_len, len( do['name' ] ) ) self.widths[ order_index ] = max_len * 5.5 @@ -95,16 +95,24 @@ class WorkflowCanvas( object ): key = out_conn_index_dict.keys()[0] out_conn_pos = self.out_pos[ out_pos_index ][ key ] adjusted = ( out_conn_pos[ 0 ] + self.widths[ output_dict[ 'id' ] ], out_conn_pos[ 1 ] ) - self.text.append( svgfig.SVG( "circle", - cx=out_conn_pos[ 0 ] + self.widths[ output_dict[ 'id' ] ] - MARGIN, - cy=out_conn_pos[ 1 ] - MARGIN, - r=5, - fill="#ffffff" ) ) - self.connectors.append( svgfig.Line( adjusted[ 0 ], - adjusted[ 1 ] - MARGIN, - in_coords[ 0 ] - 10, - in_coords[ 1 ], - arrow_end="conn_%s" % (len(self.connectors.keys()) + 1) ).SVG() ) + self.text.append( svgwrite.shapes.Circle(center=(out_conn_pos[ 0 ] + self.widths[ output_dict[ 'id' ] ] - MARGIN, + out_conn_pos[ 1 ] - MARGIN), + r=5, + fill="#ffffff", + stroke="#000000" ) ) + marker = self.canvas.marker(refX="1.5", refY="3.6", overflow='visible', + viewBox="0 0 10.5 7.2", + markerWidth="10.5", + markerHeight="7.2", + markerUnits="strokeWidth", + orient="auto", stroke="none", fill="black") + marker.add(self.canvas.path(d="M 1.5 3.6 L 0 0 L 10.5 3.6 L 0 7.2 L 1.5 3.6 Z")) + self.canvas.defs.add(marker) + conn = svgwrite.shapes.Line( (adjusted[ 0 ], adjusted[ 1 ] - MARGIN), + (in_coords[ 0 ] - 10, in_coords[ 1 ]), + stroke="#000000") + conn['marker-end'] = marker.get_funciri() + self.connectors.append( conn ) def add_steps( self, highlight_errors=False ): # Only highlight missing tools if displaying in the tool shed. @@ -114,7 +122,6 @@ class WorkflowCanvas( object ): fill = "#EBBCB2" else: fill = "#EBD9B2" - width = self.widths[ step_dict[ 'id' ] ] self.add_boxes( step_dict, width, fill ) for conn, output_dict in step_dict[ 'input_connections' ].iteritems(): @@ -129,12 +136,10 @@ class WorkflowCanvas( object ): } if tool_errors: step_dict[ 'tool_errors' ] = tool_errors - input_conn_dict = {} for conn in step.input_connections: input_conn_dict[ conn.input_name ] = \ dict( id=conn.output_step.order_index, output_name=conn.output_name ) step_dict['input_connections'] = input_conn_dict - self.data.append(step_dict) self.add_text( module_data_inputs, module_data_outputs, step, module_name ) From 0db0655b10ad47631b636300635e87530b057d94 Mon Sep 17 00:00:00 2001 From: Dannon Baker Date: Tue, 16 Feb 2016 16:01:29 -0500 Subject: [PATCH 2/7] Both uses need to .tostring() the canvas, not just the first one. --- lib/galaxy/webapps/galaxy/controllers/workflow.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/galaxy/webapps/galaxy/controllers/workflow.py b/lib/galaxy/webapps/galaxy/controllers/workflow.py index dabc0ab3d48..8b67a4366aa 100644 --- a/lib/galaxy/webapps/galaxy/controllers/workflow.py +++ b/lib/galaxy/webapps/galaxy/controllers/workflow.py @@ -514,7 +514,7 @@ class WorkflowController( BaseUIController, SharableMixin, UsesStoredWorkflowMix def gen_image( self, trans, id ): stored = self.get_stored_workflow( trans, id, check_ownership=True ) trans.response.set_content_type("image/svg+xml") - return self._workflow_to_svg_canvas( trans, stored ) + return self._workflow_to_svg_canvas( trans, stored ).tostring() @web.expose @web.require_login( "use Galaxy workflows" ) From 872bcbc4ead7694583c5d398dc799f862aff8a0a Mon Sep 17 00:00:00 2001 From: Dannon Baker Date: Tue, 16 Feb 2016 16:57:15 -0500 Subject: [PATCH 3/7] Arrow aesthetics, simplification; some magic number replacement. --- lib/galaxy/workflow/render.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/lib/galaxy/workflow/render.py b/lib/galaxy/workflow/render.py index 2376600c9e0..6bbf7ab66b0 100644 --- a/lib/galaxy/workflow/render.py +++ b/lib/galaxy/workflow/render.py @@ -37,7 +37,7 @@ class WorkflowCanvas( object ): box_height = ( len( step_dict[ 'data_inputs' ] ) + len( step_dict[ 'data_outputs' ] ) ) * LINE_SPACING + MARGIN # Draw separator line. if len( step_dict[ 'data_inputs' ] ) > 0: - box_height += 15 + box_height += LINE_SPACING sep_y = y + len( step_dict[ 'data_inputs' ] ) * LINE_SPACING + 40 self.text.append( svgwrite.shapes.Line( (x - MARGIN, sep_y), (x + width - MARGIN, sep_y ), stroke=svgwrite.rgb(0, 0, 0)) ) # Define an input/output box. @@ -62,7 +62,7 @@ class WorkflowCanvas( object ): count += 1 max_len = max( max_len, len( di[ 'label' ] ) ) if len( module_data_inputs ) > 0: - y += 15 + y += LINE_SPACING for do in module_data_outputs: cur_y = y + count * LINE_SPACING if order_index not in out_pos: @@ -100,13 +100,14 @@ class WorkflowCanvas( object ): r=5, fill="#ffffff", stroke="#000000" ) ) - marker = self.canvas.marker(refX="1.5", refY="3.6", overflow='visible', - viewBox="0 0 10.5 7.2", - markerWidth="10.5", - markerHeight="7.2", + marker = self.canvas.marker(overflow='visible', + refX="0", refY="5", + viewBox="0 0 10 5", + markerWidth="8", + markerHeight="10", markerUnits="strokeWidth", orient="auto", stroke="none", fill="black") - marker.add(self.canvas.path(d="M 1.5 3.6 L 0 0 L 10.5 3.6 L 0 7.2 L 1.5 3.6 Z")) + marker.add(self.canvas.path(d="M 0 0 L 10 5 L 0 10 z")) self.canvas.defs.add(marker) conn = svgwrite.shapes.Line( (adjusted[ 0 ], adjusted[ 1 ] - MARGIN), (in_coords[ 0 ] - 10, in_coords[ 1 ]), From f7c41623ae9abaacaf30044954f48e8ab858d1ed Mon Sep 17 00:00:00 2001 From: Dannon Baker Date: Wed, 17 Feb 2016 11:15:02 -0500 Subject: [PATCH 4/7] Add svgwrite wheel. --- lib/galaxy/dependencies/pinned-requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/galaxy/dependencies/pinned-requirements.txt b/lib/galaxy/dependencies/pinned-requirements.txt index 35516aaf539..8d4b0825d6d 100644 --- a/lib/galaxy/dependencies/pinned-requirements.txt +++ b/lib/galaxy/dependencies/pinned-requirements.txt @@ -51,7 +51,7 @@ pbr==1.8.0 six==1.9.0 Parsley==1.3 nose==1.3.7 -SVGFig==1.1.6 +svgwrite==1.1.6 # Fabric and dependencies Fabric==1.10.2 From 6f72338b449c9f92ca8a58e42f898716b0edcc74 Mon Sep 17 00:00:00 2001 From: Dannon Baker Date: Wed, 17 Feb 2016 15:34:51 -0500 Subject: [PATCH 5/7] Remove svgfig and add svgwrite hash from pinned reqs. --- lib/galaxy/dependencies/pinned-hashed-requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/galaxy/dependencies/pinned-hashed-requirements.txt b/lib/galaxy/dependencies/pinned-hashed-requirements.txt index d35ad28c5db..86eb640a84c 100644 --- a/lib/galaxy/dependencies/pinned-hashed-requirements.txt +++ b/lib/galaxy/dependencies/pinned-hashed-requirements.txt @@ -135,7 +135,7 @@ pbr==1.8.0 --hash:sha256=355a0c2ba71120dce1c522387e494c60eb4f44fa9d03067cb981618 six==1.9.0 --hash:sha256=cf806ab65d5e16561df30c2cea2fb2cf3e6aa82e48f22036dff177e20f61e188 Parsley==1.3 --hash:sha256=32fd648334146e4005dae41d559d32edfff9f3d5f503f8b5d233dbfe3a15a28b nose==1.3.7 --hash:sha256=f36054c5786e84ff6b131f3249a3cce82787f1a930ab053287c0edaa9f840d0f -SVGFig==1.1.6 --hash:sha256=300cf4b37b4773498d4ea96c703cdb5f09215978b02828dadb786fb248e0b54b +svgwrite==1.1.6 --hash:sha256=e404424dad5eaa41efec6e68daa5c0603e3f4f576ada5dca730669103ab83660 # Fabric and dependencies Fabric==1.10.2 --hash:sha256=8fe1eb0b0f271e2d41445e7da4470548d3bdf882aab1c1f143505c599996793b From 4dce3e35d75fdf94938b70a56d28ace806c907b8 Mon Sep 17 00:00:00 2001 From: Dannon Baker Date: Wed, 17 Feb 2016 15:41:02 -0500 Subject: [PATCH 6/7] Toolshed/test swaps from standalone_xml to tostring --- lib/tool_shed/util/workflow_util.py | 2 +- test/unit/workflows/test_render.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/tool_shed/util/workflow_util.py b/lib/tool_shed/util/workflow_util.py index 32e69d19465..53bcc95cbdc 100644 --- a/lib/tool_shed/util/workflow_util.py +++ b/lib/tool_shed/util/workflow_util.py @@ -202,7 +202,7 @@ def generate_workflow_image( trans, workflow_name, repository_metadata_id=None, workflow_canvas.add_steps( highlight_errors=True ) workflow_canvas.finish( ) trans.response.set_content_type( "image/svg+xml" ) - return canvas.standalone_xml() + return canvas.tostring() def get_workflow_data_inputs( step, module ): diff --git a/test/unit/workflows/test_render.py b/test/unit/workflows/test_render.py index 27bab7caf2e..b7afcd28cd1 100644 --- a/test/unit/workflows/test_render.py +++ b/test/unit/workflows/test_render.py @@ -66,4 +66,4 @@ def test_render(): ) workflow_canvas.add_steps() workflow_canvas.finish() - assert workflow_canvas.canvas.standalone_xml() + assert workflow_canvas.canvas.tostring() From c4350b4ab9e3e367f0101109b6db404d32ab0eb9 Mon Sep 17 00:00:00 2001 From: Dannon Baker Date: Mon, 29 Feb 2016 12:29:10 -0500 Subject: [PATCH 7/7] Font styling/sizing to match original behavior. --- lib/galaxy/workflow/render.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/lib/galaxy/workflow/render.py b/lib/galaxy/workflow/render.py index 6bbf7ab66b0..4b51f3a315f 100644 --- a/lib/galaxy/workflow/render.py +++ b/lib/galaxy/workflow/render.py @@ -8,6 +8,7 @@ class WorkflowCanvas( object ): def __init__( self ): self.canvas = svgwrite.Drawing(profile='full') + self.connectors = [] self.boxes = [] self.text = [] @@ -25,10 +26,10 @@ class WorkflowCanvas( object ): self.canvas.add(box) for connector in self.connectors: self.canvas.add(connector) + text_style_layer = self.canvas.g(style="font-family: Helvetica, Arial, FreeSans, Sans, sans, sans-serif;") for text in self.text: - self.canvas.add(text) - # width, height = ( max_x + max_width + 50 ), max_y + 300 - # TODO set width/height? OR flex is OK? + text_style_layer.add(text) + self.canvas.add(text_style_layer) return self.canvas def add_boxes( self, step_dict, width, name_fill ): @@ -48,7 +49,7 @@ class WorkflowCanvas( object ): x, y = left, top order_index = step.order_index max_len = len( module_name ) * 1.5 - self.text.append(svgwrite.text.Text(module_name, (x, y + 20))) + self.text.append(svgwrite.text.Text(module_name, (x, y + 20), style='font-size:14px')) y += 45 count = 0 in_pos = self.in_pos @@ -58,7 +59,7 @@ class WorkflowCanvas( object ): if order_index not in in_pos: in_pos[ order_index ] = {} in_pos[ order_index ][ di[ 'name' ] ] = ( x, cur_y ) - self.text.append( svgwrite.text.Text( di[ 'label' ], (x, cur_y) ) ) + self.text.append( svgwrite.text.Text( di[ 'label' ], (x, cur_y), style='font-size:10px' ) ) count += 1 max_len = max( max_len, len( di[ 'label' ] ) ) if len( module_data_inputs ) > 0: @@ -68,7 +69,7 @@ class WorkflowCanvas( object ): if order_index not in out_pos: out_pos[ order_index ] = {} out_pos[ order_index ][ do[ 'name' ] ] = ( x, cur_y ) - self.text.append( svgwrite.text.Text( do[ 'name' ], (x, cur_y) )) + self.text.append( svgwrite.text.Text( do[ 'name' ], (x, cur_y), style='font-size:10px' )) count += 1 max_len = max( max_len, len( do['name' ] ) ) self.widths[ order_index ] = max_len * 5.5