User manual - momapy

Skip to content

# 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

In [1]:
Copied!

    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

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
In [2]:
Copied!

    from utils import display, show_room, macromolecule_toy, production_toy

from utils import display, show_room, macromolecule_toy, production_toy

## The Map object¶

In [3]:
Copied!

    r = momapy.io.read("phospho1.sbgn")
    m = r.obj
    display(m)

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`.

In [4]:
Copied!

    momapy.utils.pretty_print(m)

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`.

In [5]:
Copied!

    momapy.utils.pretty_print(m.model)

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:

In [6]:
Copied!

    for e in m.model.entity_pools:
        break
    momapy.utils.pretty_print(e)

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`:

In [7]:
Copied!

    assert isinstance(e, momapy.sbgn.pd.EntityPool)
    assert isinstance(e, momapy.core.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:

In [8]:
Copied!

    momapy.utils.pretty_print(m.layout)

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):

In [9]:
Copied!

    momapy.utils.pretty_print(m.layout.drawing_elements())

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`:

In [10]:
Copied!

    for l in m.layout.layout_elements:
        break
    momapy.utils.pretty_print(l)

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`):

In [11]:
Copied!

    display(l)

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:

In [12]:
Copied!

    e = m.layout_model_mapping.get_mapping(l)
    momapy.utils.pretty_print(e)

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¶

In [13]:
Copied!

    r1 = momapy.io.read("phospho1.sbgn")
    m1 = r1.obj
    display(m1)

r1 = momapy.io.read("phospho1.sbgn") m1 = r1.obj display(m1)
In [14]:
Copied!

    r2 = momapy.io.read("phospho2.sbgn")
    m2 = r2.obj
    display(m2)

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:

In [15]:
Copied!

    assert m1 != m2
    assert m1.model == m2.model
    assert m1.layout != m2.layout
    assert m1.layout_model_mapping != m2.layout_model_mapping

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` of `M` is a sub-model of the `Model` of `M'`;
- the `Layout` of `M` is a sub-layout of the `Layout` of `M'`;
- and the `LayoutModelMapping` of `M` is a sub-mapping of the `LayoutModelMapping` of `M'`.

#### Example 1¶

In [16]:
Copied!

    r1 = momapy.io.read("phospho1.sbgn")
    m1 = r1.obj
    display(m1)

r1 = momapy.io.read("phospho1.sbgn") m1 = r1.obj display(m1)
In [17]:
Copied!

    r3 = momapy.io.read("phospho3.sbgn")
    m3 = r3.obj
    display(m3)

r3 = momapy.io.read("phospho3.sbgn") m3 = r3.obj display(m3)

The second map is an excerpt of the first map:

In [18]:
Copied!

    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)

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¶

In [19]:
Copied!

    r4 = momapy.io.read("phospho4.sbgn")
    m4 = r4.obj
    display(m4)

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:

In [20]:
Copied!

    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)

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:

In [21]:
Copied!

    r = momapy.io.read("phospho1.sbgn")
    m = r.obj
    display(m)

r = momapy.io.read("phospho1.sbgn") m = r.obj display(m)
In [22]:
Copied!

    for l in m.layout.layout_elements:
        break
    display(l)

for l in m.layout.layout_elements: break display(l)
In [23]:
Copied!

    try:
        l.stroke_width = 3.0
    except Exception as e:
        print(e)

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:

In [24]:
Copied!

    lb = momapy.builder.builder_from_object(l)
    momapy.utils.pretty_print(lb)

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

In [25]:
Copied!

    lb.stroke_width = 3.0
    l = momapy.builder.object_from_builder(lb)
    display(l)

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:

In [26]:
Copied!

    l = momapy.builder.object_from_builder(lb)

l = momapy.builder.object_from_builder(lb)
In [27]:
Copied!

    assert l.stroke_width == 3.0

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:

In [28]:
Copied!

    r = momapy.io.read("phospho1.sbgn")
    m = r.obj
    momapy.io.write(m, "phospho1_output.sbgn", writer="sbgnml")

r = momapy.io.read("phospho1.sbgn") m = r.obj momapy.io.write(m, "phospho1_output.sbgn", writer="sbgnml")

## Rendering¶

In [29]:
Copied!

    r = momapy.io.read("phospho1.sbgn")
    m = r.obj
    display(m)

r = momapy.io.read("phospho1.sbgn") m = r.obj display(m)

`Map`s can be rendered in different formats using a simple render function:

In [30]:
Copied!

    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")

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:

In [31]:
Copied!

    momapy.rendering.core.render_map(m, "phospho1.pdf", format_="pdf", to_top_left=True)

momapy.rendering.core.render_map(m, "phospho1.pdf", format_="pdf", to_top_left=True)
In [32]:
Copied!

    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

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:

In [33]:
Copied!

    momapy.rendering.core.render_maps([m1, m2, m3, m4], "phospho_multi.pdf", format_="pdf", multi_pages=True)

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:

In [34]:
Copied!

    r = momapy.io.read("phospho1.sbgn")
    m = r.obj
    display(m)

r = momapy.io.read("phospho1.sbgn") m = r.obj display(m)
In [35]:
Copied!

    for l in m.layout.layout_elements:
        break
    display(l)

for l in m.layout.layout_elements: break display(l)
In [36]:
Copied!

    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)

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...

In [37]:
Copied!

    l = momapy.builder.object_from_builder(lb)
    display(l)

l = momapy.builder.object_from_builder(lb) display(l)

The `label` of a `Node` can also be styled:

In [38]:
Copied!

    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)

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:

In [39]:
Copied!

    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)

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.

In [40]:
Copied!

    r = momapy.io.read("phospho1.sbgn")
    m = r.obj
    display(m)

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:

In [41]:
Copied!

    m = momapy.styling.apply_style_sheet(m, momapy.sbgn.styling.newt)
    display(m)

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`:

In [42]:
Copied!

    m = momapy.sbgn.utils.newt_tidy(m)
    display(m)

m = momapy.sbgn.utils.newt_tidy(m) display(m)
In [43]:
Copied!

    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.sbgned) m = momapy.sbgn.utils.sbgned_tidy(m) display(m)
In [44]:
Copied!

    m = momapy.styling.apply_style_sheet(m, momapy.sbgn.styling.cs_default)
    display(m)

m = momapy.styling.apply_style_sheet(m, momapy.sbgn.styling.cs_default) display(m)
In [45]:
Copied!

    m = momapy.styling.apply_style_sheet(m, momapy.sbgn.styling.fs_shadows)
    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:

In [46]:
Copied!

    with open("../src/momapy/sbgn/styling/sbgned_no_cs.css") as f:
        for line in f.readlines()[:35]:
            print(line[:-1])

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:

In [47]:
Copied!

    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)

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¶

In [48]:
Copied!

    l = macromolecule_toy()
    display(l)

l = macromolecule_toy() display(l)

#### Anchors¶

`Node`s have anchor points, that are specific `Point`s on their border:

In [49]:
Copied!

    l.north_west()

l.north_west()
Out[49]:

    Point(x=80.01, y=77.51)

In [50]:
Copied!

    display(l, l.north_west())

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`

In [51]:
Copied!

    show_room(momapy.sbgn.pd.MacromoleculeLayout)

show_room(momapy.sbgn.pd.MacromoleculeLayout)
In [52]:
Copied!

    show_room(momapy.sbgn.pd.GenericProcessLayout)

show_room(momapy.sbgn.pd.GenericProcessLayout)
In [53]:
Copied!

    show_room(momapy.sbgn.pd.NucleicAcidFeatureMultimerLayout)

show_room(momapy.sbgn.pd.NucleicAcidFeatureMultimerLayout)

#### Angles¶

`Node`s also have angle points:

In [54]:
Copied!

    l.self_angle(130)

l.self_angle(130)
Out[54]:

    Point(x=161.97, y=75.0)

In [55]:
Copied!

    display(l, l.self_angle(130))

display(l, l.self_angle(130))
In [56]:
Copied!

    show_room(momapy.sbgn.pd.MacromoleculeLayout, "self_angle")

show_room(momapy.sbgn.pd.MacromoleculeLayout, "self_angle")
In [57]:
Copied!

    display(l, l.angle(130))

display(l, l.angle(130))
In [58]:
Copied!

    show_room(momapy.sbgn.pd.MacromoleculeLayout, "angle")

show_room(momapy.sbgn.pd.MacromoleculeLayout, "angle")

### For Arcs¶

In [59]:
Copied!

    l = production_toy()
    display(l)

l = production_toy() display(l)

#### Anchors¶

Analogously to `Node`s, `Arc`s have a few anchor points:

In [60]:
Copied!

    l.arrowhead_base()

l.arrowhead_base()
Out[60]:

    Point(x=303.79, y=71.21)

In [61]:
Copied!

    display(l, l.arrowhead_base())

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`

In [62]:
Copied!

    show_room(momapy.sbgn.pd.ProductionLayout)

show_room(momapy.sbgn.pd.ProductionLayout)

#### Fraction¶

`Arc`s also have fraction points:

In [63]:
Copied!

    l.fraction(0.50)

l.fraction(0.50)
Out[63]:

    (Point(x=225.0, y=150.0), 5.497787143782138)

In [64]:
Copied!

    pos, angle = l.fraction(0.50)
    display(l, pos)

pos, angle = l.fraction(0.50) display(l, pos)
In [65]:
Copied!

    show_room(momapy.sbgn.pd.ProductionLayout, "fraction")

show_room(momapy.sbgn.pd.ProductionLayout, "fraction")

## Relative positioning¶

Automatic geometry enables positioning `LayoutElement`s relatively to one another:

In [66]:
Copied!

    r = momapy.io.read("phospho1.sbgn")
    m = r.obj
    display(m)

r = momapy.io.read("phospho1.sbgn") m = r.obj display(m)
In [67]:
Copied!

    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

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
In [68]:
Copied!

    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)

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:

In [69]:
Copied!

    momapy.positioning.fit(mb.layout.layout_elements, xsep=20, ysep=10)

momapy.positioning.fit(mb.layout.layout_elements, xsep=20, ysep=10)
Out[69]:

    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:

In [70]:
Copied!

    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)

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`):

In [71]:
Copied!

    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)

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¶

In [72]:
Copied!

    import dataclasses

import dataclasses
In [73]:
Copied!

    @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]

@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]
In [74]:
Copied!

    show_room(MyTriangle)

show_room(MyTriangle)
In [75]:
Copied!

    show_room(MyTriangle, "angle")

show_room(MyTriangle, "angle")

### New Arc¶

In [76]:
Copied!

    @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]

@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]
In [77]:
Copied!

    show_room(MyRectangleArrow)

show_room(MyRectangleArrow)
In [78]:
Copied!

    show_room(MyRectangleArrow, "fraction")

show_room(MyRectangleArrow, "fraction")

New `LayoutElement`s can be even more easily programmed using the `Shape`s already available in the `meta` module:

In [79]:
Copied!

    import momapy.meta.shapes

import momapy.meta.shapes
In [80]:
Copied!

    @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()

@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()
In [81]:
Copied!

    show_room(MyDoubleRectangleArrow)

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