Skip to content

GraphML Export

The graphml render type produces a GraphML document that can be opened directly in yEd Graph Editor. Edges are colored on a six-stop perceptual gradient derived from a composite narrative tension score.


How to export

# Start the API server
docker compose up -d

# Ingest a narrative
curl -X POST http://localhost:8000/v1/notes/import \
  -H "Content-Type: application/json" \
  -d '{"title": "My Story", "text": "Alice locked the door. Bob arrived and tried the handle. He could not enter."}'

# Export as GraphML (replace <narrative_id> with the returned ID)
curl -X POST http://localhost:8000/v1/render/<narrative_id> \
  -H "Content-Type: application/json" \
  -d '{"type": "graphml"}' \
  | jq -r '.content' > my_story.graphml

Opening in yEd

  1. Launch yEd Graph Editor (version 3.x or later).
  2. File → Open and select my_story.graphml.
  3. yEd reads the yFiles extension keys (d3 for node graphics, d6 for edge graphics) and applies colors automatically.
  4. Use Layout → Hierarchical (or Organic) for a readable layout — yEd does not auto-layout on import.
  5. The Properties panel shows each node/edge's tension_score attribute (d8 key), useful for filtering high-tension edges.

Node color legend

Node type Color Hex
Narrative Blue #4A90D9
Scene Green #7ED321
Atom Amber #F5A623
Event Red #D0021B
PatternInstance Purple #9B59B6
Pattern Dark red #C0392B
Perspective Teal #1ABC9C
MoodState Coral #E74C3C
GenreProfile Sky blue #3498DB
Chronotope Dark green #27AE60
CodeTag Orange #F39C12
Transform Grey #95A5A6
Character Violet #8E44AD

Edge tension scoring

Tension is a composite score in [0.0, 1.0] combining three signals:

1. Relationship type (base score)

Relationship Base score Rationale
PREVENTS 0.9 Highest conflict; direct opposition of agency
CAUSES 0.7 Strong causal force; irreversible consequence
PARTICIPATES_IN 0.4 Character involvement; stakes present
ENABLES 0.4 Facilitation; indirect force
PRECEDES 0.2 Temporal sequence; low inherent tension
Structural (HAS_SCENE, CONTAINS, etc.) 0.0 Containment; no narrative force

2. Barthesian code modifier (additive)

Only the highest-ranking code tag in the source scene contributes, preventing inflation from heavily-tagged scenes.

Code Bonus Rationale
HERMENEUTIC +0.4 Unresolved mystery — maximum reader tension
PROAIRETIC +0.3 Imminent action — suspense
SYMBOLIC +0.2 Thematic opposition — latent tension
SEMIC +0.1 Connotative load — background unease
CULTURAL +0.0 Shared knowledge — neutral

3. Scene mood modifier (additive)

High arousal combined with negative valence produces the anxious mood profile most associated with narrative tension.

mood_bonus = arousal × max(0, −valence) × 0.6

Maximum mood contribution: +0.6 (arousal = 1.0, valence = −1.0).

Final score

tension = clamp(base + code_bonus + mood_bonus, 0.0, 1.0)

Edge color gradient

Six stops map the [0.0, 1.0] range to a perceptual gradient. Colors were chosen to remain distinguishable under common forms of color-vision deficiency (the grey → blue axis is distinguishable from grey → red for protanopia / deuteranopia).

graph LR
    A["0.0 · #A0A0A0 · grey"]
    B["0.2 · #4682B4 · steel-blue"]
    C["0.4 · #DAA520 · gold"]
    D["0.6 · #FF8C00 · orange"]
    E["0.8 · #DC143C · crimson"]
    F["1.0 · #8B0000 · dark-red"]
    A --> B --> C --> D --> E --> F
Stop Score Color Hex
1 0.0 Grey #A0A0A0
2 0.2 Steel blue #4682B4
3 0.4 Goldenrod #DAA520
4 0.6 Dark orange #FF8C00
5 0.8 Crimson #DC143C
6 1.0 Dark red #8B0000

Colors between stops are linearly interpolated in RGB space.


Architecture

The GraphML feature follows the existing Strategy renderer pattern:

classDiagram
    class RendererProtocol {
        <<Protocol>>
        +render(graph_state, params) RenderOutput
    }
    class GraphMLRenderer {
        +render(graph_state, params) RenderOutput
    }
    class TensionScorer {
        +score_edge(relation_type, atoms, mood) TensionScore
        +score_structural_edge() TensionScore
    }
    class _GraphMLBuilder {
        -_walk()
        -_serialise() str
    }
    RendererProtocol <|.. GraphMLRenderer
    GraphMLRenderer --> _GraphMLBuilder
    _GraphMLBuilder --> TensionScorer
  • tension_scorer.py — pure scoring functions; no XML or graph concerns.
  • graphml_renderer.py — XML construction only; delegates tension scoring.
  • Neither module issues Cypher; all data arrives as a GraphState snapshot from RenderService.

Module reference

tng.renderers.graphml_renderer

GraphML renderer — yEd-compatible export with tension-coloured edges.

Implements RendererProtocol. Traverses a GraphState snapshot and produces a GraphML document that can be opened directly in yEd Graph Editor.

Node colouring by label type:

+-------------------+--------------------+ | Node label | Fill colour | +===================+====================+ | Narrative | #4A90D9 (blue) | | Scene | #7ED321 (green) | | Atom | #F5A623 (amber) | | Event | #D0021B (red) | | PatternInstance | #9B59B6 (purple) | | Pattern | #C0392B (dark-red) | | Perspective | #1ABC9C (teal) | | MoodState | #E74C3C (coral) | | GenreProfile | #3498DB (sky-blue) | | Chronotope | #27AE60 (dark-grn) | | CodeTag | #F39C12 (orange) | | Transform | #95A5A6 (grey) | | Character | #8E44AD (violet) | +-------------------+--------------------+

Edge colouring by narrative tension:

Edges are coloured by a composite tension score computed by tension_scorer.score_edge(). The six-stop gradient runs from neutral grey (0.0) through steel-blue → gold → orange → crimson to dark-red (1.0).

yEd import: File → Open → select the .graphml file. The yFiles extension keys (d3/d6 for node/edge graphics) are automatically interpreted when the file is opened in yEd 3.x or later.

GraphMLRenderer

Renders a GraphState as a yEd-compatible GraphML document.

Edges are coloured by narrative tension (see tension_scorer). Structural containment edges (HAS_SCENE, CONTAINS, etc.) are rendered in neutral grey; causal/preventive event relations are coloured on the full tension gradient.

:method render: Convert GraphState to a GraphML XML string.

Source code in src/tng/renderers/graphml_renderer.py
class GraphMLRenderer:
    """Renders a ``GraphState`` as a yEd-compatible GraphML document.

    Edges are coloured by narrative tension (see ``tension_scorer``).
    Structural containment edges (HAS_SCENE, CONTAINS, etc.) are rendered
    in neutral grey; causal/preventive event relations are coloured on the
    full tension gradient.

    :method render: Convert ``GraphState`` to a GraphML XML string.
    """

    def render(self, graph_state: GraphState, params: dict[str, Any]) -> RenderOutput:
        """Produce a yEd-compatible GraphML document from the graph state.

        :param graph_state: Complete narrative snapshot.
        :param params: Optional renderer parameters (currently unused).
        :returns: ``RenderOutput`` with ``content_type="application/xml"``.
        """
        builder = _GraphMLBuilder(graph_state)
        xml_string = builder.build()

        narrative = graph_state.narrative
        node_count = builder.node_count
        edge_count = builder.edge_count
        logger.info(
            "GraphMLRenderer: narrative=%s nodes=%d edges=%d",
            narrative.id,
            node_count,
            edge_count,
        )
        return RenderOutput(
            content=xml_string,
            content_type="application/xml",
            metadata={
                "narrative_id": narrative.id,
                "node_count": node_count,
                "edge_count": edge_count,
                "format": "graphml-yed",
            },
        )

render(graph_state, params)

Produce a yEd-compatible GraphML document from the graph state.

Parameters:

Name Type Description Default
graph_state GraphState

Complete narrative snapshot.

required
params dict[str, Any]

Optional renderer parameters (currently unused).

required

Returns:

Type Description
RenderOutput

RenderOutput with content_type="application/xml".

Source code in src/tng/renderers/graphml_renderer.py
def render(self, graph_state: GraphState, params: dict[str, Any]) -> RenderOutput:
    """Produce a yEd-compatible GraphML document from the graph state.

    :param graph_state: Complete narrative snapshot.
    :param params: Optional renderer parameters (currently unused).
    :returns: ``RenderOutput`` with ``content_type="application/xml"``.
    """
    builder = _GraphMLBuilder(graph_state)
    xml_string = builder.build()

    narrative = graph_state.narrative
    node_count = builder.node_count
    edge_count = builder.edge_count
    logger.info(
        "GraphMLRenderer: narrative=%s nodes=%d edges=%d",
        narrative.id,
        node_count,
        edge_count,
    )
    return RenderOutput(
        content=xml_string,
        content_type="application/xml",
        metadata={
            "narrative_id": narrative.id,
            "node_count": node_count,
            "edge_count": edge_count,
            "format": "graphml-yed",
        },
    )

tng.renderers.tension_scorer

Narrative tension scoring for GraphML edge visualisation.

Tension is a composite score in [0.0, 1.0] derived from three sources:

  1. Relationship type — causal/preventive relations carry more tension than temporal ones (base scores defined in RELATION_BASE).
  2. Barthesian codes — atoms tagged with HERMENEUTIC or PROAIRETIC codes signal unresolved mystery or imminent action, boosting tension.
  3. Scene mood — high arousal combined with negative valence produces the anxious state associated with peak narrative tension.

The final score is clamped to [0.0, 1.0] and mapped through a six-stop perceptual gradient from neutral grey to deep red. The colour values are chosen so that even viewers with common forms of colour-vision deficiency can perceive the low→high gradient (grey→blue is distinguishable from grey→red for protanopia/deuteranopia).

TensionScore dataclass

Tension assessment for a single edge.

Parameters:

Name Type Description Default
score float

Composite tension value in [0.0, 1.0].

required
hex_color str

Six-character hex RGB string (e.g. "#DC143C").

required
base float

Base score from relationship type alone.

required
code_bonus float

Additive modifier from Barthesian codes.

required
mood_bonus float

Additive modifier from scene mood.

required
Source code in src/tng/renderers/tension_scorer.py
@dataclass(frozen=True)
class TensionScore:
    """Tension assessment for a single edge.

    :param score: Composite tension value in [0.0, 1.0].
    :param hex_color: Six-character hex RGB string (e.g. ``"#DC143C"``).
    :param base: Base score from relationship type alone.
    :param code_bonus: Additive modifier from Barthesian codes.
    :param mood_bonus: Additive modifier from scene mood.
    """

    score: float
    hex_color: str
    base: float
    code_bonus: float
    mood_bonus: float

score_edge(relation_type, atoms=None, mood=None)

Compute a tension score for a single graph edge.

Parameters:

Name Type Description Default
relation_type str

The Cypher relationship type string (e.g. "CAUSES", "HAS_SCENE").

required
atoms Sequence[Atom] | None

Atoms in the source scene whose code tags contribute Barthesian code modifiers. Pass None or empty to skip.

None
mood MoodState | None

The active MoodState of the source scene. Pass None to skip the mood contribution.

None

Returns:

Type Description
TensionScore

A TensionScore with the final score and rendered colour.

Source code in src/tng/renderers/tension_scorer.py
def score_edge(
    relation_type: str,
    atoms: Sequence[Atom] | None = None,
    mood: MoodState | None = None,
) -> TensionScore:
    """Compute a tension score for a single graph edge.

    :param relation_type: The Cypher relationship type string
        (e.g. ``"CAUSES"``, ``"HAS_SCENE"``).
    :param atoms: Atoms in the source scene whose code tags contribute
        Barthesian code modifiers.  Pass ``None`` or empty to skip.
    :param mood: The active ``MoodState`` of the source scene.  Pass ``None``
        to skip the mood contribution.
    :returns: A ``TensionScore`` with the final score and rendered colour.
    """
    base = RELATION_BASE.get(relation_type, _STRUCTURAL_BASE)

    code_bonus = _code_bonus(atoms or [])
    mood_bonus = _mood_bonus(mood)

    raw = base + code_bonus + mood_bonus
    clamped = max(0.0, min(1.0, raw))

    return TensionScore(
        score=round(clamped, 4),
        hex_color=_interpolate_color(clamped),
        base=base,
        code_bonus=round(code_bonus, 4),
        mood_bonus=round(mood_bonus, 4),
    )

score_structural_edge(label='')

Return a zero-tension score for structural/containment edges.

Parameters:

Name Type Description Default
label str

Optional label for the edge (unused, kept for call-site readability).

''

Returns:

Type Description
TensionScore

A TensionScore at 0.0 mapped to grey.

Source code in src/tng/renderers/tension_scorer.py
def score_structural_edge(label: str = "") -> TensionScore:
    """Return a zero-tension score for structural/containment edges.

    :param label: Optional label for the edge (unused, kept for call-site
        readability).
    :returns: A ``TensionScore`` at 0.0 mapped to grey.
    """
    return score_edge(label or "_structural")