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
The Map object¶
r = momapy.io.read("phospho1.sbgn")
m = r.obj
display(m)
Fontconfig warning: "/usr/share/fontconfig/conf.avail/05-reset-dirs-sample.conf", line 6: unknown element "reset-dirs"
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_='66d3ca29-f792-4... * id_: <class 'str'> = 66d3ca29-f792-4c65-bb22-287a2b... * model: <class 'momapy.sbgn.pd.SBGNPDModel'> = SBGNPDModel(id_='19b33a6e-5462... * layout: <class 'momapy.sbgn.pd.SBGNPDLayout'> = SBGNPDLayout(id_='55d19664-a9e... * layout_model_mapping: momapy.core.LayoutModelMapping | None = LayoutModelMapping({StateVaria...
The Model object¶
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_='19b33a6e-5462... * id_: <class 'str'> = 19b33a6e-5462-4947-963e-c550d0... * 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 ModelElement
s or collections of ModelElements
. For example, the entity_pools
attribute of an SBGNPDModel
may contain zero or more EntityPool
s.
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)
The Layout object¶
A Layout
is some sort of canvas that may contain other LayoutElement
s that correspond to shapes that represent ModelElement
s and that may be rendered.
In SBGN, LayoutElement
s are either Node
s, Arc
s or TextLayout
s.
The different LayoutElement
s of a Layout
are contained in its layout_elements
attribute:
momapy.utils.pretty_print(m.layout)
<class 'momapy.sbgn.pd.SBGNPDLayout'>: SBGNPDLayout(id_='55d19664-a9e... * id_: <class 'str'> = 55d19664-a9e0-44de-b95c-db46bb... * 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: tuple[float, ...] | None = None * group_stroke_dashoffset: float | None = None * group_stroke_width: 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: tuple[float, ...] | None = None * stroke_dashoffset: float | None = None * stroke_width: 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 DrawingElement
s that may be rendered using a Renderer
.
The DrawingElement
s are built on the fly based on the other attributes of the LayoutElement
.
DrawingElement
s 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 LayoutElement
s, which themselves may contain other LayoutElement
s, recursively forming a hierarchy of LayoutElement
s, and thus of DrawingElement
s.
While a Layout
represents a Model
, contained LayoutElement
s represent ModelElement
s 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: tuple[float, ...] | None = None * group_stroke_dashoffset: float | None = None * group_stroke_width: 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_='f5e0c99d-d167-... * 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: tuple[float, ...] | None = None * stroke_dashoffset: 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 LayoutElement
s in its layout_elements
attribute, it will also produce the DrawingElement
s of these contained LayoutElement
s (here, a StateVariableLayout
):
display(l)
The LayoutModelMapping object¶
A LayoutModelMapping
is a mapping from LayoutElement
s to ModelElement
s and vice-versa.
It is used to map the LayoutElement
s of a Map
to the ModelElement
s 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¶
Map
s, Model
s, Layout
s, LayoutModelMapping
s, ModelElement
s and LayoutElement
s are programmed 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 Map
s 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 Map
s are equal if and only if:
- their
Model
s are equal; - their
Layout
s are equal; - and their
LayoutModelMapping
s 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
Model
ofM
is a sub-model of theModel
ofM'
; - the
Layout
ofM
is a sub-layout of theLayout
ofM'
; - and the
LayoutModelMapping
ofM
is a sub-mapping of theLayoutModelMapping
ofM'
.
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[tuple[float, ...], NoneType, momapy.core.TupleBuilder[float, ...]] = None * group_stroke_dashoffset: typing.Optional[float] = None * group_stroke_width: typing.Optional[float] = 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_='efc0442... * 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[tuple[float, ...], NoneType, momapy.core.TupleBuilder[float, ...]] = None * stroke_dashoffset: typing.Optional[float] = 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¶
SBGNMap
s 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)
Map
s 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")
Layout
s 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 Map
s 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 LayoutElement
s:
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: tuple[float, ...] | None = None * stroke_dashoffset: float | None = None * stroke_width: 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: tuple[float, ...] | None = (5, 5) * stroke_dashoffset: float | None = None * stroke_width: 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: tuple[float, ...] | None = None * stroke_dashoffset: float | None = None * stroke_width: 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: tuple[float, ...] | None = None * stroke_dashoffset: float | None = None * stroke_width: 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 LayoutElement
s:
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 StyleSheet
s 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("""
SBGNPDLayout {
stroke: red;
fill: lightyellow;
}
MacromoleculeLayout {
fill: green;
}
""")
my_style_sheet = momapy.styling.StyleSheet.from_file("my_style_sheet.css")
m = momapy.styling.apply_style_sheet(m, my_style_sheet)
display(m)
Automatic geometry¶
LayoutElement
s support automatic geometry: their "shape" can be automatically computed from the DrawingElement
s 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¶
Node
s have anchor points, that are specific Point
s on their border:
l.north_west()
Point(x=80.01, y=77.51)
display(l, l.north_west())
All Node
s have at least the following anchor points:
north_west
north
north_east
east
south_east
south
south_west
west
center
label_center
show_room(momapy.sbgn.pd.MacromoleculeLayout)
show_room(momapy.sbgn.pd.GenericProcessLayout)
show_room(momapy.sbgn.pd.NucleicAcidFeatureMultimerLayout)
Angles¶
Node
s 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 Node
s, Arc
s have a few anchor points:
l.arrowhead_base()
Point(x=303.79, y=71.21)
display(l, l.arrowhead_base())
All Arc
s have at least the following anchor points:
end_point
start_point
SingleHeadedArcs
have the following additional anchor points:
arrowhead_base
arrowhead_tip
And DoubleHeadedArcs
have the following additional ones:
start_arrowhead_base
start_arrowhead_tip
end_arrowhead_base
end_arrowhead_tip
show_room(momapy.sbgn.pd.ProductionLayout)
Fraction¶
Arc
s 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 LayoutElement
s 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 Node
s, 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 Node
s and Arc
s can be easily programmed.
Since the geometry is automatic, it is only required to program the general shape of the LayoutElement
using DrawingElement
s.
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 LayoutElement
s can be even more easily programmed using the Shape
s 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
- Improve performance of geometry (currently slow)
Future work¶
- Support for background images and gradients
- Support for SBGN ER maps
- Support for BioPAX models
- Automatic XML/JSON format
- Developpment of converters