momapy: a python library for molecular maps¶
The momapy library is a new python library for working with molecular maps such as SBGN maps. Its key feature is its definition of a map, that is now formed of two entities: a model, that describes what concepts are represented, and a layout, that describes how these concepts are represented. This definition is borrowed from SBML and its extensions layout/render, that allowed users to add a layout to an SBML model. MomaPy aims at extending this definition to all types of molecular maps, and in particular to SBGN maps.
MomaPy offers the following features:
- support for SBGN PD and AF maps (read/write SBGN-ML with annotations, rendering information, and notes)
- decomposition of a map object into:
- a model object;
- a layout object;
- a mapping between the model and layout objects' subelements
- map, model, layout and mapping objects comparison; fast object in set checking
- rendering of maps to images (SVG, PDF, JPEG, PNG, WebP) and other surfaces (e.g. GLFW window)
- support for styling and css like stylesheets (including effects such as shadows)
- automatic geometry and anchors (for arcs, shape borders)
- local positioning (e.g. right of shape, fit set of shapes)
- easy extension with new model and layout subelements
import momapy.io
import momapy.builder
import momapy.coloring
import momapy.styling
import momapy.utils
import momapy.sbgn.io.sbgnml
import momapy.sbgn.styling
import momapy.sbgn.utils
from utils import display, show_room, macromolecule_toy, production_toy
Maps¶
r = momapy.io.read("phospho1.sbgn")
m = r.obj
display(m)
A Map object always contains a Model, a Layout and a LayoutModelMapping (that maps model elements to layout elements).
It may also have additional attributes depending on its nature. For example, an SBGN PD map also has an id.
momapy.utils.pretty_print(m)
<class 'momapy.sbgn.pd.SBGNPDMap'>: SBGNPDMap(id_='982f902c-e9a5-4... * id_: <class 'str'> = 982f902c-e9a5-4dc5-9098-0e51ec... * model: <class 'momapy.sbgn.pd.SBGNPDModel'> = SBGNPDModel(id_='b3896614-8f60... * layout: <class 'momapy.sbgn.pd.SBGNPDLayout'> = SBGNPDLayout(id_='6d4675b0-fbe... * layout_model_mapping: momapy.core.LayoutModelMapping | None = LayoutModelMapping({StateVaria...
Models¶
A Model may have an arbitrary number of attributes, depending on its nature.
For example, an SBGNPDModel has the following attributes: entity_pools, processes, compartments, modulations, logical_operators, equivalence_operators, submaps and tags, but also an id.
momapy.utils.pretty_print(m.model)
<class 'momapy.sbgn.pd.SBGNPDModel'>: SBGNPDModel(id_='b3896614-8f60... * id_: <class 'str'> = b3896614-8f60-4a22-a1d1-580d72... * entity_pools: frozenset[momapy.sbgn.pd.EntityPool] = frozenset({Macromolecule(id_='... * processes: frozenset[momapy.sbgn.pd.Process] = frozenset({GenericProcess(id_=... * compartments: frozenset[momapy.sbgn.pd.Compartment] = frozenset() * modulations: frozenset[momapy.sbgn.pd.Modulation] = frozenset({Catalysis(id_='arc3... * logical_operators: frozenset[momapy.sbgn.pd.LogicalOperator] = frozenset() * equivalence_operators: frozenset[momapy.sbgn.pd.EquivalenceOperator] = frozenset() * submaps: frozenset[momapy.sbgn.pd.Submap] = frozenset() * tags: frozenset[momapy.sbgn.pd.Tag] = frozenset()
These attributes may be ModelElements or collections of ModelElements. For example, the entity_pools attribute of an SBGNPDModel may contain zero or more EntityPools.
We pick the first element from the collection:
for e in m.model.entity_pools:
break
momapy.utils.pretty_print(e)
<class 'momapy.sbgn.pd.Macromolecule'>: Macromolecule(id_='glyph3', co... * id_: <class 'str'> = glyph3 * compartment: momapy.sbgn.pd.Compartment | None = None * label: str | None = B * state_variables: frozenset[momapy.sbgn.pd.StateVariable] = frozenset() * units_of_information: frozenset[momapy.sbgn.pd.UnitOfInformation] = frozenset()
Here, the element is a Macromolecule.
The data model for SBGN PD is built on a hierarchy of classes following the corresponding ontology.
Hence this element is also an EntityPool, and more generally a ModelElement:
assert isinstance(e, momapy.sbgn.pd.EntityPool)
assert isinstance(e, momapy.core.ModelElement)
Layouts¶
A Layout is some sort of canvas that may contain other LayoutElements that correspond to shapes that represent ModelElements and that may be rendered.
In SBGN, LayoutElements are either Nodes, Arcs or TextLayouts.
The different LayoutElements of a Layout are contained in its layout_elements attribute:
momapy.utils.pretty_print(m.layout)
<class 'momapy.sbgn.pd.SBGNPDLayout'>: SBGNPDLayout(id_='6d4675b0-fbe... * id_: <class 'str'> = 6d4675b0-fbec-4ebf-9a41-2c4f30... * layout_elements: tuple[momapy.core.LayoutElement] = (MacromoleculeLayout(id_='glyp... * group_fill: momapy.drawing.NoneValueType | momapy.coloring.Color | None = None * group_fill_rule: momapy.drawing.FillRule | None = None * group_filter: momapy.drawing.NoneValueType | momapy.drawing.Filter | None = None * group_font_family: str | None = None * group_font_size: float | None = None * group_font_style: momapy.drawing.FontStyle | None = None * group_font_weight: momapy.drawing.FontWeight | float | None = None * group_stroke: momapy.drawing.NoneValueType | momapy.coloring.Color | None = None * group_stroke_dasharray: momapy.drawing.NoneValueType | tuple[float, ...] | None = None * group_stroke_dashoffset: momapy.drawing.NoneValueType | float | None = None * group_stroke_width: momapy.drawing.NoneValueType | float | None = None * group_text_anchor: momapy.drawing.TextAnchor | None = None * group_transform: momapy.drawing.NoneValueType | tuple[momapy.geometry.Transformation] | None = None * fill: momapy.drawing.NoneValueType | momapy.coloring.Color | None = Color(red=255, green=255, blue... * filter: momapy.drawing.NoneValueType | momapy.drawing.Filter | None = None * height: <class 'float'> = 250.0 * label: momapy.core.TextLayout | None = None * position: <class 'momapy.geometry.Point'> = Point(x=240.0, y=205.0) * stroke: momapy.drawing.NoneValueType | momapy.coloring.Color | None = None * stroke_dasharray: momapy.drawing.NoneValueType | tuple[float, ...] | None = None * stroke_dashoffset: momapy.drawing.NoneValueType | float | None = None * stroke_width: momapy.drawing.NoneValueType | float | None = None * transform: momapy.drawing.NoneValueType | tuple[momapy.geometry.Transformation] | None = None * width: <class 'float'> = 450.0
A LayoutElement always has a drawing_elements method that returns DrawingElements that may be rendered using a Renderer.
The DrawingElements are built on the fly based on the other attributes of the LayoutElement.
DrawingElements are like SVG elements: they may represent paths, rectangle, ellipses, text..., and generally have the same attributes as their SVG counterpart (or a subset of them).
A Layout itself is a Node, whose returned unique DrawingElement ultimately represents a rectangle built from its position, width and height attributes.
The style of this rectangle depends on the styling attribute of the Layout (e.g. the stroke, stroke_width, and fill attributes):
momapy.utils.pretty_print(m.layout.drawing_elements())
<class 'list'>: [Group(class_='SBGNPDLayout_gr... - 0: <class 'momapy.drawing.Group'> = Group(class_='SBGNPDLayout_gro...
A Layout may contain other LayoutElements, which themselves may contain other LayoutElements, recursively forming a hierarchy of LayoutElements, and thus of DrawingElements.
While a Layout represents a Model, contained LayoutElements represent ModelElements contained by the Model.
We pick the first LayoutElement from the SBGNPDLayout:
for l in m.layout.layout_elements:
break
momapy.utils.pretty_print(l)
<class 'momapy.sbgn.pd.MacromoleculeLayout'>: MacromoleculeLayout(id_='glyph... * id_: <class 'str'> = glyph2 * layout_elements: tuple[momapy.core.LayoutElement] = (StateVariableLayout(id_='glyp... * group_fill: momapy.drawing.NoneValueType | momapy.coloring.Color | None = None * group_fill_rule: momapy.drawing.FillRule | None = None * group_filter: momapy.drawing.NoneValueType | momapy.drawing.Filter | None = None * group_font_family: str | None = None * group_font_size: float | None = None * group_font_style: momapy.drawing.FontStyle | None = None * group_font_weight: momapy.drawing.FontWeight | float | None = None * group_stroke: momapy.drawing.NoneValueType | momapy.coloring.Color | None = None * group_stroke_dasharray: momapy.drawing.NoneValueType | tuple[float, ...] | None = None * group_stroke_dashoffset: momapy.drawing.NoneValueType | float | None = None * group_stroke_width: momapy.drawing.NoneValueType | float | None = None * group_text_anchor: momapy.drawing.TextAnchor | None = None * group_transform: momapy.drawing.NoneValueType | tuple[momapy.geometry.Transformation] | None = None * fill: momapy.drawing.NoneValueType | momapy.coloring.Color | None = Color(red=255, green=255, blue... * filter: momapy.drawing.NoneValueType | momapy.drawing.Filter | None = None * height: <class 'float'> = 60.0 * label: momapy.core.TextLayout | None = TextLayout(id_='4bf48500-7af0-... * position: <class 'momapy.geometry.Point'> = Point(x=85.0, y=270.0) * stroke: momapy.drawing.NoneValueType | momapy.coloring.Color | None = Color(red=0, green=0, blue=0, ... * stroke_dasharray: momapy.drawing.NoneValueType | tuple[float, ...] | None = None * stroke_dashoffset: momapy.drawing.NoneValueType | float | None = None * stroke_width: float | None = 1.25 * transform: momapy.drawing.NoneValueType | tuple[momapy.geometry.Transformation] | None = None * width: <class 'float'> = 108.0 * rounded_corners: <class 'float'> = 5.0
This element is a MacromoleculeLayout. Based on its position, width and height attributes, as well as on its styling attributes, it will produce a DrawingElement representing a rectangle with rounded corners, containing some text corresponding to its label. Since this element also contains other LayoutElements in its layout_elements attribute, it will also produce the DrawingElements of these contained LayoutElements (here, a StateVariableLayout):
display(l)
LayoutModelMappings¶
A LayoutModelMapping is a mapping from LayoutElements to ModelElements and vice-versa.
It is used to map the LayoutElements of a Map to the ModelElements they represent.
We pick the ModelElement mapped to the MacromoleculeLayout we had picked:
e = m.layout_model_mapping.get_mapping(l)
momapy.utils.pretty_print(e)
<class 'momapy.sbgn.pd.Macromolecule'>: Macromolecule(id_='glyph2', co... * id_: <class 'str'> = glyph2 * compartment: momapy.sbgn.pd.Compartment | None = None * label: str | None = A * state_variables: frozenset[momapy.sbgn.pd.StateVariable] = frozenset({StateVariable(id_='... * units_of_information: frozenset[momapy.sbgn.pd.UnitOfInformation] = frozenset()
Equality and-sub relation for Maps, Models, Layouts and LayoutModelMappings¶
Maps, Models, Layouts, LayoutModelMappings, ModelElements and LayoutElements are encoded so they can be easily compared.
Their identity relies on the value of a subset of their attributes (generally all attributes but their id_).
This way, two Maps can be easily compared.
Is is also possible to check whether a Map is a sub-map of another Map (not to be confused with SBGN PD's submap glyph).
Equality¶
Definition¶
Two Maps are equal if and only if:
- their
Models are equal; - their
Layouts are equal; - and their
LayoutModelMappings are equal.
Example¶
r1 = momapy.io.read("phospho1.sbgn")
m1 = r1.obj
display(m1)
r2 = momapy.io.read("phospho2.sbgn")
m2 = r2.obj
display(m2)
The two maps represent the exact same concepts, and thus have the same model. However, they do not have the same layout. Hence, the two maps are different.
This can be checked easily by comparing the Map, Model, Layout and LayoutModelMapping objects representing the two maps:
assert m1 != m2
assert m1.model == m2.model
assert m1.layout != m2.layout
assert m1.layout_model_mapping != m2.layout_model_mapping
Sub-map/model/layout/mapping¶
Definition¶
A Map M is a sub-map of another Map M' if and only if:
- the
ModelofMis a sub-model of theModelofM'; - the
LayoutofMis a sub-layout of theLayoutofM'; - and the
LayoutModelMappingofMis a sub-mapping of theLayoutModelMappingofM'.
Example 1¶
r1 = momapy.io.read("phospho1.sbgn")
m1 = r1.obj
display(m1)
r3 = momapy.io.read("phospho3.sbgn")
m3 = r3.obj
display(m3)
The second map is an excerpt of the first map:
assert m3.is_submap(m1)
assert m3.model.is_submodel(m1.model)
assert m3.layout.is_sublayout(m1.layout)
assert m3.layout_model_mapping.is_submapping(m1.layout_model_mapping)
Example 2¶
r4 = momapy.io.read("phospho4.sbgn")
m4 = r4.obj
display(m4)
Because of the compartment, the model of the first map is not an excerpt of the model of the second map. However, the layout of the first map is an excerpt of the layout of the second map:
assert not m3.is_submap(m4)
assert not m3.model.is_submodel(m4.model)
assert m3.layout.is_sublayout(m4.layout)
assert not m3.layout_model_mapping.is_submapping(m4.layout_model_mapping)
Frozen and builder objects¶
Map, Model, Layout, ModelElement and LayoutElement objects cannot be modified; they are frozen:
r = momapy.io.read("phospho1.sbgn")
m = r.obj
display(m)
for l in m.layout.layout_elements:
break
display(l)
try:
l.stroke_width = 3.0
except Exception as e:
print(e)
cannot assign to field 'stroke_width'
This way they can be hashed, which is necessary to check whether a Map object belongs to a set efficiently, for example.
However, we want to be able to modify them programmatically (e.g., change the stroke width of the border of a shape).
To this end, each class has a corresponding builder class, that allows the production of objects that are not frozen.
Such objects may be built directly from the frozen objects:
lb = momapy.builder.builder_from_object(l)
momapy.utils.pretty_print(lb)
<class 'momapy.builder.MacromoleculeLayoutBuilder'>: MacromoleculeLayoutBuilder(id_... * id_: <class 'str'> = glyph2 * layout_elements: typing.Union[tuple[momapy.core.LayoutElement], momapy.core.TupleBuilder[momapy.builder.LayoutElementBuilder]] = [StateVariableLayoutBuilder(id... * group_fill: typing.Union[momapy.drawing.NoneValueType, momapy.coloring.Color, NoneType, momapy.builder.ColorBuilder] = None * group_fill_rule: typing.Optional[momapy.drawing.FillRule] = None * group_filter: typing.Union[momapy.drawing.NoneValueType, momapy.drawing.Filter, NoneType, momapy.builder.FilterBuilder] = None * group_font_family: typing.Optional[str] = None * group_font_size: typing.Optional[float] = None * group_font_style: typing.Optional[momapy.drawing.FontStyle] = None * group_font_weight: typing.Union[momapy.drawing.FontWeight, float, NoneType] = None * group_stroke: typing.Union[momapy.drawing.NoneValueType, momapy.coloring.Color, NoneType, momapy.builder.ColorBuilder] = None * group_stroke_dasharray: typing.Union[momapy.drawing.NoneValueType, tuple[float, ...], NoneType, momapy.core.TupleBuilder[float, ...]] = None * group_stroke_dashoffset: typing.Union[momapy.drawing.NoneValueType, float, NoneType] = None * group_stroke_width: typing.Union[momapy.drawing.NoneValueType, float, NoneType] = None * group_text_anchor: typing.Optional[momapy.drawing.TextAnchor] = None * group_transform: typing.Union[momapy.drawing.NoneValueType, tuple[momapy.geometry.Transformation], NoneType, momapy.core.TupleBuilder[momapy.builder.TransformationBuilder]] = None * fill: typing.Union[momapy.drawing.NoneValueType, momapy.coloring.Color, NoneType, momapy.builder.ColorBuilder] = ColorBuilder(red=255, green=25... * filter: typing.Union[momapy.drawing.NoneValueType, momapy.drawing.Filter, NoneType, momapy.builder.FilterBuilder] = None * height: <class 'float'> = 60.0 * label: typing.Union[momapy.core.TextLayout, NoneType, momapy.builder.TextLayoutBuilder] = TextLayoutBuilder(id_='d910c93... * position: typing.Union[momapy.geometry.Point, momapy.builder.PointBuilder, NoneType] = PointBuilder(x=85.0, y=270.0) * stroke: typing.Union[momapy.drawing.NoneValueType, momapy.coloring.Color, NoneType, momapy.builder.ColorBuilder] = ColorBuilder(red=0, green=0, b... * stroke_dasharray: typing.Union[momapy.drawing.NoneValueType, tuple[float, ...], NoneType, momapy.core.TupleBuilder[float, ...]] = None * stroke_dashoffset: typing.Union[momapy.drawing.NoneValueType, float, NoneType] = None * stroke_width: typing.Optional[float] = 1.25 * transform: typing.Union[momapy.drawing.NoneValueType, tuple[momapy.geometry.Transformation], NoneType, momapy.core.TupleBuilder[momapy.builder.TransformationBuilder]] = None * width: <class 'float'> = 108.0 * rounded_corners: <class 'float'> = 5.0
lb.stroke_width = 3.0
l = momapy.builder.object_from_builder(lb)
display(l)
The frozen object may then be built back from the builder:
l = momapy.builder.object_from_builder(lb)
assert l.stroke_width == 3.0
Reading and writing¶
SBGNMaps may be read from and written to SBGN-ML files using read and write functions:
r = momapy.io.read("phospho1.sbgn")
m = r.obj
momapy.io.write(m, "phospho1_output.sbgn", writer="sbgnml")
Rendering¶
r = momapy.io.read("phospho1.sbgn")
m = r.obj
display(m)
Maps can be rendered in different formats using a simple render function:
momapy.rendering.core.render_map(m, "phospho1.pdf", format_="pdf")
momapy.rendering.core.render_map(m, "phospho1.png", format_="png")
momapy.rendering.core.render_map(m, "phospho1.svg", format_="svg")
momapy.rendering.core.render_map(m, "phospho1.webp", format_="webp")
momapy.rendering.core.render_map(m, "phospho1.jpeg", format_="jpeg")
Fontconfig warning: "/usr/share/fontconfig/conf.avail/05-reset-dirs-sample.conf", line 6: unknown element "reset-dirs"
Layouts can be moved to the top left using the top_left option:
momapy.rendering.core.render_map(m, "phospho1.pdf", format_="pdf", to_top_left=True)
r1 = momapy.io.read("phospho1.sbgn")
m1 = r1.obj
r2 = momapy.io.read("phospho2.sbgn")
m2 = r2.obj
r3 = momapy.io.read("phospho3.sbgn")
m3 = r3.obj
r4 = momapy.io.read("phospho4.sbgn")
m4 = r4.obj
Multiple Maps can be rendered in one document using a simple function:
momapy.rendering.core.render_maps([m1, m2, m3, m4], "phospho_multi.pdf", format_="pdf", multi_pages=True)
Styling¶
Basic styling¶
Basic styling can be easily applied to LayoutElements:
r = momapy.io.read("phospho1.sbgn")
m = r.obj
display(m)
for l in m.layout.layout_elements:
break
display(l)
lb = momapy.builder.builder_from_object(l)
lb.fill = momapy.coloring.lightblue
lb.stroke = momapy.coloring.brown
lb.stroke_width = 3.0
lb.stroke_dasharray = (5, 5)
momapy.utils.pretty_print(lb.drawing_elements(), max_depth=3)
<class 'list'>: [Group(class_='MacromoleculeLa... - 0: <class 'momapy.drawing.Group'> = Group(class_='MacromoleculeLay... <class 'momapy.drawing.Group'>: Group(class_='MacromoleculeLay... * class_: str | None = MacromoleculeLayoutBuilder_gro... * fill: momapy.drawing.NoneValueType | momapy.coloring.Color | None = None * fill_rule: momapy.drawing.FillRule | None = None * filter: momapy.drawing.NoneValueType | momapy.drawing.Filter | None = None * font_family: str | None = None * font_size: float | None = None * font_style: momapy.drawing.FontStyle | None = None * font_weight: momapy.drawing.FontWeight | int | None = None * id_: str | None = glyph2_group * stroke: momapy.drawing.NoneValueType | momapy.coloring.Color | None = None * stroke_dasharray: momapy.drawing.NoneValueType | tuple[float, ...] | None = None * stroke_dashoffset: momapy.drawing.NoneValueType | float | None = None * stroke_width: momapy.drawing.NoneValueType | float | None = None * text_anchor: momapy.drawing.TextAnchor | None = None * transform: momapy.drawing.NoneValueType | tuple[momapy.geometry.Transformation] | None = None * elements: tuple[momapy.drawing.DrawingElement] = (Group(class_='MacromoleculeLa... <class 'tuple'>: (Group(class_='MacromoleculeLa... - 0: <class 'momapy.drawing.Group'> = Group(class_='MacromoleculeLay... <class 'momapy.drawing.Group'>: Group(class_='MacromoleculeLay... * class_: str | None = MacromoleculeLayoutBuilder * fill: momapy.drawing.NoneValueType | momapy.coloring.Color | None = Color(red=173, green=216, blue... * fill_rule: momapy.drawing.FillRule | None = None * filter: momapy.drawing.NoneValueType | momapy.drawing.Filter | None = None * font_family: str | None = None * font_size: float | None = None * font_style: momapy.drawing.FontStyle | None = None * font_weight: momapy.drawing.FontWeight | int | None = None * id_: str | None = glyph2 * stroke: momapy.drawing.NoneValueType | momapy.coloring.Color | None = Color(red=165, green=42, blue=... * stroke_dasharray: momapy.drawing.NoneValueType | tuple[float, ...] | None = (5, 5) * stroke_dashoffset: momapy.drawing.NoneValueType | float | None = None * stroke_width: momapy.drawing.NoneValueType | float | None = 3.0 * text_anchor: momapy.drawing.TextAnchor | None = None * transform: momapy.drawing.NoneValueType | tuple[momapy.geometry.Transformation] | None = None * elements: tuple[momapy.drawing.DrawingElement] = (Path(class_=None, fill=None, ... - 1: <class 'momapy.drawing.Text'> = Text(class_=None, fill=ColorBu... <class 'momapy.drawing.Text'>: Text(class_=None, fill=ColorBu... * class_: str | None = None * fill: momapy.drawing.NoneValueType | momapy.coloring.Color | None = ColorBuilder(red=0, green=0, b... * fill_rule: momapy.drawing.FillRule | None = None * filter: momapy.drawing.NoneValueType | momapy.drawing.Filter | None = None * font_family: str | None = Helvetica * font_size: float | None = 14.0 * font_style: momapy.drawing.FontStyle | None = FontStyle.NORMAL * font_weight: momapy.drawing.FontWeight | int | None = FontWeight.NORMAL * id_: str | None = None * stroke: momapy.drawing.NoneValueType | momapy.coloring.Color | None = <momapy.drawing.NoneValueType ... * stroke_dasharray: momapy.drawing.NoneValueType | tuple[float, ...] | None = None * stroke_dashoffset: momapy.drawing.NoneValueType | float | None = None * stroke_width: momapy.drawing.NoneValueType | float | None = None * text_anchor: momapy.drawing.TextAnchor | None = None * transform: momapy.drawing.NoneValueType | tuple[momapy.geometry.Transformation] | None = None * text: <class 'str'> = A * point: <class 'momapy.geometry.Point'> = Point(x=80.5, y=273.5) - 2: <class 'momapy.drawing.Group'> = Group(class_='StateVariableLay... <class 'momapy.drawing.Group'>: Group(class_='StateVariableLay... * class_: str | None = StateVariableLayoutBuilder_gro... * fill: momapy.drawing.NoneValueType | momapy.coloring.Color | None = None * fill_rule: momapy.drawing.FillRule | None = None * filter: momapy.drawing.NoneValueType | momapy.drawing.Filter | None = None * font_family: str | None = None * font_size: float | None = None * font_style: momapy.drawing.FontStyle | None = None * font_weight: momapy.drawing.FontWeight | int | None = None * id_: str | None = glyph2a_group * stroke: momapy.drawing.NoneValueType | momapy.coloring.Color | None = None * stroke_dasharray: momapy.drawing.NoneValueType | tuple[float, ...] | None = None * stroke_dashoffset: momapy.drawing.NoneValueType | float | None = None * stroke_width: momapy.drawing.NoneValueType | float | None = None * text_anchor: momapy.drawing.TextAnchor | None = None * transform: momapy.drawing.NoneValueType | tuple[momapy.geometry.Transformation] | None = None * elements: tuple[momapy.drawing.DrawingElement] = (Group(class_='StateVariableLa...
l = momapy.builder.object_from_builder(lb)
display(l)
The label of a Node can also be styled:
lb.label.font_family = "Times"
lb.label.font_size = 30.0
lb.label.fill = momapy.coloring.red
lb.label.stroke = momapy.coloring.black
lb.label.stroke_width = 2.0
l = momapy.builder.object_from_builder(lb)
display(l)
Advanced styling¶
Advanced effects such as transformations (translation, rotation, ...) and filter effects can be applied to LayoutElements:
lb.group_transform = (momapy.geometry.Scaling(2, 1), momapy.geometry.Rotation(0.5, lb.position),)
lb.group_filter = momapy.drawing.Filter(effects=(momapy.drawing.DropShadowEffect(dx=3.0, dy=3.0, std_deviation=5.0, flood_opacity=0.5, flood_color=momapy.coloring.blue),))
l = momapy.builder.object_from_builder(lb)
display(l)
CSS-like style sheets¶
Styles may be applied to a Map using a StyleSheet. A StyleSheet can be built from a text document whose syntax is a subset of the CSS syntax.
r = momapy.io.read("phospho1.sbgn")
m = r.obj
display(m)
There are pre-built StyleSheet objects for SBGN-ED and Newt-like styles, for colors, and for shadows:
m = momapy.styling.apply_style_sheet(m, momapy.sbgn.styling.newt)
display(m)
Applying a StyleSheet to a Map may change the size of the nodes.
Some simple functions can be used to tidy the Map:
m = momapy.sbgn.utils.newt_tidy(m)
display(m)
m = momapy.styling.apply_style_sheet(m, momapy.sbgn.styling.sbgned)
m = momapy.sbgn.utils.sbgned_tidy(m)
display(m)
m = momapy.styling.apply_style_sheet(m, momapy.sbgn.styling.cs_default)
display(m)
m = momapy.styling.apply_style_sheet(m, momapy.sbgn.styling.fs_shadows)
display(m)
These pre-built StyleSheets are built from CSS-like text files:
with open("../src/momapy/sbgn/styling/sbgned_no_cs.css") as f:
for line in f.readlines()[:35]:
print(line[:-1])
AndOperatorLayout {
filter: unset;
stroke-dasharray: unset;
stroke-dashoffset: unset;
stroke-width: 2.0;
transform: unset;
filter: unset;
group-font-family: unset;
group-font-size: unset;
group-font-style: unset;
group-font-weight: unset;
height: 42.0;
left-connector-filter: unset;
left-connector-length: 10.0;
left-connector-stroke-dasharray: unset;
left-connector-stroke-dashoffset: unset;
left-connector-stroke-width: unset;
right-connector-filter: unset;
right-connector-length: 10.0;
right-connector-stroke-dasharray: unset;
right-connector-stroke-dashoffset: unset;
right-connector-stroke-width: unset;
stroke-dasharray: unset;
stroke-dashoffset: unset;
stroke-width: unset;
transform: unset;
width: 42.0;
}
AssociationLayout {
filter: unset;
stroke-dasharray: unset;
stroke-dashoffset: unset;
stroke-width: 2.0;
One may build a StyleSheet from a file and apply it to a Map with simple functions:
with open("my_style_sheet.css", "w") as f:
f.write("""
StateVariableLayout {
fill: royalblue;
stroke: white;
stroke-width: 1.5;
group-filter: unset;
}
StateVariableLayout > TextLayout {
font-size: 7.0;
fill: white;
}
MacromoleculeLayout {
fill: royalblue;
stroke: white;
stroke-width: 2.0;
group-filter: unset;
}
MacromoleculeLayout > TextLayout {
font-size: 14.0;
fill: white;
}
SimpleChemicalLayout {
fill: gold;
stroke: white;
stroke-width: 2.0;
group-filter: unset;
}
SimpleChemicalLayout > TextLayout {
font-size: 10.0;
fill: black;
}
GenericProcessLayout {
fill: gray;
stroke: white;
right-connector-stroke: gray;
right-connector-length: 10.0;
right-connector-stroke-width: 1.5;
left-connector-stroke: gray;
left-connector-length: 10.0;
left-connector-stroke-width: 1.5;
group-filter: unset;
}
ConsumptionLayout {
arrowhead-fill: white;
arrowhead-stroke: black;
path-stroke: gray;
path-stroke-width: 1.5;
end-shorten: 1.0;
group-filter: unset;
}
ProductionLayout {
arrowhead-fill: gray;
arrowhead-stroke: gray;
path-stroke: gray;
path-stroke-width: 1.5;
end-shorten: 1.0;
group-filter: unset;
}
CatalysisLayout {
arrowhead-fill: white;
arrowhead-stroke: gray;
arrowhead-stroke-width: 2.0;
path-stroke: gray;
path-stroke-width: 1.5;
end-shorten: 1.0;
group-filter: unset;
stroke-width: 1.0;
}
""")
The map with no style sheet:
m = momapy.io.read("mek_erk.sbgn").obj
display(m, scale=1.5)
The map with the style sheet applied, and tided:
my_style_sheet = momapy.styling.StyleSheet.from_file("my_style_sheet.css")
m = momapy.styling.apply_style_sheet(m, my_style_sheet)
m = momapy.sbgn.utils.newt_tidy(m)
display(m, scale=1.5)
Automatic geometry¶
LayoutElements support automatic geometry: their "shape" can be automatically computed from the DrawingElements they return, and be accessed with various methods.
These methods depend on the nature of the LayoutElement (Node or Arc).
For Nodes¶
l = macromolecule_toy()
display(l)
Anchors¶
Nodes have anchor points, that are specific Points on their border:
l.north_west()
Point(x=80.01, y=77.51)
display(l, l.north_west())
All Nodes have at least the following anchor points:
north_westnorthnorth_easteastsouth_eastsouthsouth_westwestcenterlabel_center
show_room(momapy.sbgn.pd.MacromoleculeLayout)
show_room(momapy.sbgn.pd.GenericProcessLayout)
show_room(momapy.sbgn.pd.NucleicAcidFeatureMultimerLayout)
Angles¶
Nodes also have angle points:
l.self_angle(130)
Point(x=161.97, y=75.0)
display(l, l.self_angle(130))
show_room(momapy.sbgn.pd.MacromoleculeLayout, "self_angle")
display(l, l.angle(130))
show_room(momapy.sbgn.pd.MacromoleculeLayout, "angle")
For Arcs¶
l = production_toy()
display(l)
Anchors¶
Analogously to Nodes, Arcs have a few anchor points:
l.arrowhead_base()
Point(x=303.79, y=71.21)
display(l, l.arrowhead_base())
All Arcs have at least the following anchor points:
end_pointstart_point
SingleHeadedArcs have the following additional anchor points:
arrowhead_basearrowhead_tip
And DoubleHeadedArcs have the following additional ones:
start_arrowhead_basestart_arrowhead_tipend_arrowhead_baseend_arrowhead_tip
show_room(momapy.sbgn.pd.ProductionLayout)
Fraction¶
Arcs also have fraction points:
l.fraction(0.50)
(Point(x=225.0, y=150.0), 5.497787143782138)
pos, angle = l.fraction(0.50)
display(l, pos)
show_room(momapy.sbgn.pd.ProductionLayout, "fraction")
Relative positioning¶
Automatic geometry enables positioning LayoutElements relatively to one another:
r = momapy.io.read("phospho1.sbgn")
m = r.obj
display(m)
mb = momapy.builder.builder_from_object(m)
for lb in mb.layout.layout_elements:
if hasattr(lb, "label") and lb.label is not None and lb.label.text == "B": # we select the layout for B
eb = lb
elif momapy.builder.isinstance_or_builder(lb, momapy.sbgn.pd.GenericProcessLayout): # we select the process layout
pb = lb
eb.position = momapy.positioning.above_left_of(pb, 200, 50) # eb's position is set 200 units above and 50 units left of pb's position
eb.label.position = eb.position
momapy.sbgn.utils.tidy(mb) # sets the arcs to the borders
m = momapy.builder.object_from_builder(mb)
display(m)
The following functions are available:
- above_left_of
- above_of
- above_right_of
- right_of
- below_right_of
- below_of
- below_left_of
- left_of
For container Nodes, the fit function is also available:
momapy.positioning.fit(mb.layout.layout_elements, xsep=20, ysep=10)
Bbox(position=Point(x=237.5, y=164.0), width=453.0, height=292.0)
Each of the above functions has a corresponding set function, which directly sets the returned value(s) to the correct Node's attributes:
mb.layout.stroke = momapy.coloring.red
momapy.positioning.set_fit(mb.layout, mb.layout.layout_elements, xsep=20, ysep=10)
m = momapy.builder.object_from_builder(mb)
display(m)
The set functions have an anchor option that sets the relative target anchor of the Node receiving the new position (default is center):
momapy.positioning.set_above_left_of(eb, pb, 200, 50, anchor="south_east") # eb's position is set such that its south_east anchor is 200 units above and 50 units left of pb's position
eb.label.position = eb.position
momapy.sbgn.utils.tidy(mb)
m = momapy.builder.object_from_builder(mb)
display(m)
Building new types of Nodes and Arcs¶
New types of Nodes and Arcs can be easily programmed.
Since the geometry is automatic, it is only required to program the general shape of the LayoutElement using DrawingElements.
New Node¶
import dataclasses
@dataclasses.dataclass(frozen=True)
class MyTriangle(momapy.core.Node):
height: float = 30.0
width: float = 30.0
fill: momapy.coloring.Color = momapy.coloring.white
stroke: momapy.coloring.Color = momapy.coloring.black
def _border_drawing_elements(self):
actions = [
momapy.drawing.MoveTo(self.position - (0, self.height / 2)), # top
momapy.drawing.LineTo(self.position + (self.width / 2, self.height / 2)), # bottom right
momapy.drawing.LineTo(self.position + (-self.width / 2, self.height / 2)), # bottom left
momapy.drawing.ClosePath()
]
path = momapy.drawing.Path(actions=actions)
return [path]
show_room(MyTriangle)
show_room(MyTriangle, "angle")
New Arc¶
@dataclasses.dataclass(frozen=True)
class MyRectangleArrow(momapy.core.SingleHeadedArc):
arrowhead_width: float = 10.0
arrowhead_height: float = 10.0
arrowhead_fill: momapy.coloring.Color | momapy.drawing.NoneValueType = momapy.coloring.white
arrowhead_stroke: momapy.coloring.Color = momapy.coloring.black
path_fill: momapy.coloring.Color | momapy.drawing.NoneValueType = momapy.drawing.NoneValue
path_stroke: momapy.coloring.Color = momapy.coloring.black
def _arrowhead_border_drawing_elements(self):
actions = [
momapy.drawing.MoveTo(momapy.geometry.Point(0,0)), # we draw the arrowhead as if its base is at (0, 0)
momapy.drawing.LineTo(momapy.geometry.Point(0, -self.arrowhead_height / 2)), # top left
momapy.drawing.LineTo(momapy.geometry.Point(self.arrowhead_width, -self.arrowhead_height / 2)), # top right
momapy.drawing.LineTo(momapy.geometry.Point(self.arrowhead_width, self.arrowhead_height / 2)), # bottom right
momapy.drawing.LineTo(momapy.geometry.Point(0, self.arrowhead_height / 2)), # bottom left
momapy.drawing.ClosePath()
]
path = momapy.drawing.Path(actions=actions)
return [path]
show_room(MyRectangleArrow)
show_room(MyRectangleArrow, "fraction")
New LayoutElements can be even more easily programmed using the Shapes already available in the meta module:
import momapy.meta.shapes
@dataclasses.dataclass(frozen=True)
class MyDoubleRectangleArrow(momapy.core.DoubleHeadedArc):
start_arrowhead_width: float = 10.0
start_arrowhead_height: float = 10.0
start_arrowhead_fill: momapy.coloring.Color | momapy.drawing.NoneValueType = momapy.coloring.white
start_arrowhead_stroke: momapy.coloring.Color = momapy.coloring.black
end_arrowhead_width: float = 20.0
end_arrowhead_height: float = 20.0
end_arrowhead_fill: momapy.coloring.Color | momapy.drawing.NoneValueType = momapy.coloring.white
end_arrowhead_stroke: momapy.coloring.Color = momapy.coloring.black
path_fill: momapy.coloring.Color | momapy.drawing.NoneValueType = momapy.drawing.NoneValue
path_stroke: momapy.coloring.Color = momapy.coloring.black
def _start_arrowhead_border_drawing_elements(self):
return momapy.meta.shapes.Rectangle(
position=momapy.geometry.Point(-self.start_arrowhead_width/2, 0),
width=self.start_arrowhead_width,
height=self.start_arrowhead_height
).drawing_elements()
def _end_arrowhead_border_drawing_elements(self):
return momapy.meta.shapes.Rectangle(
position=momapy.geometry.Point(self.end_arrowhead_width/2, 0),
width=self.end_arrowhead_width,
height=self.end_arrowhead_height
).drawing_elements()
show_room(MyDoubleRectangleArrow)
Ongoing and future work¶
Ongoing work¶
- Support for SBML layout/render
Future work¶
- Improve performance of geometry (currently slow)
- Support for background images and gradients in layouts
- Support for SBGN ER maps?
- Support for BioPAX models?
- Automatic XML/JSON format
- Developpment of converters