yog_io

Package Version Hex Docs

Graph file format I/O for the yog graph library. Provides serialization and deserialization support for popular graph file formats including TGF, LEDA, Pajek, JSON, GraphML, and GDF.

Features

Installation

Add yog_io to your Gleam project:

gleam add yog_io

Quick Start

import yog/model.{Directed}
import yog_io

pub fn main() {
  // Create a graph
  let graph =
    model.new(Directed)
    |> model.add_node(1, "Alice")
    |> model.add_node(2, "Bob")
    |> model.add_node(3, "Charlie")

  let assert Ok(graph) =
    model.add_edges(graph, [
      #(1, 2, "friend"),
      #(2, 3, "colleague"),
    ])

  // Write to GraphML
  let assert Ok(Nil) = yog_io.write_graphml("graph.graphml", graph)

  // Read from GraphML
  let assert Ok(loaded) = yog_io.read_graphml("graph.graphml")

  // Or use GDF format
  let assert Ok(Nil) = yog_io.write_gdf("graph.gdf", graph)
  let assert Ok(loaded_gdf) = yog_io.read_gdf("graph.gdf")

  // Or export to JSON for web visualization
  let assert Ok(Nil) = yog_io.write_json("graph.json", graph)
  let json_string = yog_io.to_json(graph)
}

Usage

GraphML Format

GraphML is an XML-based format widely supported by graph visualization tools.

import yog/model.{Directed}
import yog_io/graphml

// Basic serialization for String graphs
let graph =
  model.new(Directed)
  |> model.add_node(1, "Alice")
  |> model.add_node(2, "Bob")

let assert Ok(graph) = model.add_edge(graph, from: 1, to: 2, with: "5")

// Serialize to GraphML XML string
let xml = graphml.serialize(graph)

// Write to file
let assert Ok(Nil) = graphml.write("graph.graphml", graph)

// Read from file
let assert Ok(loaded) = graphml.read("graph.graphml")

Custom Types with GraphML

Use custom attribute mappers to serialize your domain types:

import gleam/dict
import gleam/int
import gleam/result
import yog/model.{Directed}
import yog_io/graphml

// Define your domain types
type Person {
  Person(name: String, age: Int, role: String)
}

type Relationship {
  Relationship(kind: String, strength: Int)
}

// Create a graph with custom types
let graph =
  model.new(Directed)
  |> model.add_node(1, Person("Alice", 30, "Engineer"))
  |> model.add_node(2, Person("Bob", 25, "Designer"))

let assert Ok(graph) =
  model.add_edge(
    graph,
    from: 1,
    to: 2,
    with: Relationship("friend", 8),
  )

// Define attribute mappers
let node_attr = fn(person: Person) {
  dict.from_list([
    #("name", person.name),
    #("age", int.to_string(person.age)),
    #("role", person.role),
  ])
}

let edge_attr = fn(rel: Relationship) {
  dict.from_list([
    #("kind", rel.kind),
    #("strength", int.to_string(rel.strength)),
  ])
}

// Serialize with custom mappers
let xml = graphml.serialize_with(node_attr, edge_attr, graph)

// Deserialize with custom mappers
let node_folder = fn(attrs) {
  Person(
    name: dict.get(attrs, "name") |> result.unwrap(""),
    age: dict.get(attrs, "age")
      |> result.unwrap("0")
      |> int.parse()
      |> result.unwrap(0),
    role: dict.get(attrs, "role") |> result.unwrap(""),
  )
}

let edge_folder = fn(attrs) {
  Relationship(
    kind: dict.get(attrs, "kind") |> result.unwrap(""),
    strength: dict.get(attrs, "strength")
      |> result.unwrap("0")
      |> int.parse()
      |> result.unwrap(0),
  )
}

let assert Ok(loaded) =
  graphml.deserialize_with(node_folder, edge_folder, xml)

Gephi Compatibility

For use with Gephi, use typed attributes to enable proper numeric visualizations, weighted layouts, and statistical analysis:

import gleam/dict
import gleam/float
import gleam/int
import yog/model.{Directed}
import yog_io/graphml.{DoubleType, IntType, StringType}

type Person {
  Person(name: String, age: Int, influence: Float)
}

let graph =
  model.new(Directed)
  |> model.add_node(1, Person("Alice", 30, 0.85))
  |> model.add_node(2, Person("Bob", 25, 0.92))

let assert Ok(graph) = model.add_edge(graph, from: 1, to: 2, with: 5.0)

// Map to typed attributes for Gephi
let node_attrs = fn(p: Person) {
  dict.from_list([
    #("label", #(p.name, StringType)),
    #("age", #(int.to_string(p.age), IntType)),
    #("influence", #(float.to_string(p.influence), DoubleType)),
  ])
}

let edge_attrs = fn(weight: Float) {
  dict.from_list([
    #("weight", #(float.to_string(weight), DoubleType)),
  ])
}

// Write with proper types for Gephi
let assert Ok(Nil) = graphml.write_with_types(
  "graph.graphml",
  node_attrs,
  edge_attrs,
  graph,
)

With typed attributes, Gephi can:

See GEPHI.md for complete Gephi compatibility guide.

GDF Format

GDF (GUESS Graph Format) is a simple CSV-like format with separate sections for nodes and edges.

import yog/model.{Directed}
import yog_io/gdf

// Basic serialization for String graphs
let graph =
  model.new(Directed)
  |> model.add_node(1, "Alice")
  |> model.add_node(2, "Bob")

let assert Ok(graph) = model.add_edge(graph, from: 1, to: 2, with: "friend")

// Serialize to GDF string
let gdf_string = gdf.serialize(graph)

// Serialize with integer weights
let weighted_graph =
  model.new(Directed)
  |> model.add_node(1, "A")
  |> model.add_node(2, "B")

let assert Ok(weighted_graph) =
  model.add_edge(weighted_graph, from: 1, to: 2, with: 42)

let gdf_weighted = gdf.serialize_weighted(weighted_graph)

// Write to file
let assert Ok(Nil) = gdf.write("graph.gdf", graph)

// Read from file
let assert Ok(loaded) = gdf.read("graph.gdf")

GDF Output Format

nodedef>name VARCHAR,label VARCHAR
1,Alice
2,Bob
edgedef>node1 VARCHAR,node2 VARCHAR,directed BOOLEAN,label VARCHAR
1,2,true,friend

Custom Options for GDF

import yog_io/gdf

// Customize separator and type annotations
let options = gdf.GdfOptions(
  separator: ";",
  include_types: False,
  include_directed: Some(True),
)

let gdf_string = gdf.serialize_with(node_attr, edge_attr, options, graph)

JSON Format

JSON format export for web visualization libraries and data exchange. Provides multiple format presets for popular visualization tools and supports full bidirectional I/O.

import yog/model.{Directed}
import yog_io/json
import gleam/dynamic/decode

// Export to JSON string
let json_string = json.to_json(graph, json.default_export_options())

// Import from JSON string
let assert Ok(graph) = json.from_json(json_string, decode.string, decode.string)
import yog/model.{Directed}
import yog_io/json

// Basic serialization for String graphs
let assert Ok(graph) =
  model.new(Directed)
  |> model.add_node(1, "Alice")
  |> model.add_node(2, "Bob")
  |> model.add_edge(from: 1, to: 2, with: "follows")

// Export to JSON string with default options
let json_string = json.to_json(graph, json.default_export_options())

// Export to file (simple method)
let assert Ok(Nil) = json.write("graph.json", graph)

// Export to file (with custom options)
let assert Ok(Nil) = json.write_with(
  "graph.json",
  json.default_export_options(),
  graph,
)

Format Presets

The JSON module supports multiple format presets for different visualization libraries:

D3.js Force-Directed Format

import gleam/json as gleam_json
import gleam/option

let d3_options = json.JsonExportOptions(
  format: json.D3Force,
  include_metadata: False,
  node_serializer: option.Some(gleam_json.string),
  edge_serializer: option.Some(gleam_json.string),
  pretty: True,
  metadata: option.None,
)

let d3_json = json.to_json(graph, d3_options)
// Or use the convenience function
let d3_json = json.to_d3_json(graph, gleam_json.string, gleam_json.string)

Cytoscape.js Format

let cyto_json = json.to_cytoscape_json(graph, gleam_json.string, gleam_json.string)

vis.js Format

let visjs_json = json.to_visjs_json(graph, gleam_json.string, gleam_json.string)

NetworkX Format (Python compatibility)

let nx_options = json.JsonExportOptions(
  format: json.NetworkX,
  include_metadata: False,
  node_serializer: option.Some(gleam_json.string),
  edge_serializer: option.Some(gleam_json.string),
  pretty: True,
  metadata: option.None,
)

let nx_json = json.to_json(graph, nx_options)

Custom Types with JSON

Use custom serializers to export graphs with any data types:

import gleam/dict
import gleam/json as gleam_json
import gleam/option

pub type Person {
  Person(name: String, age: Int, role: String)
}

let assert Ok(graph) =
  model.new(Directed)
  |> model.add_node(1, Person("Alice", 30, "Engineer"))
  |> model.add_node(2, Person("Bob", 25, "Designer"))
  |> model.add_edge(from: 1, to: 2, with: 5)

let options = json.export_options_with(
  fn(person: Person) {
    gleam_json.object([
      #("name", gleam_json.string(person.name)),
      #("age", gleam_json.int(person.age)),
      #("role", gleam_json.string(person.role)),
    ])
  },
  fn(weight) { gleam_json.int(weight) },
)

let json_string = json.to_json(graph, options)

JSON with Metadata

Add custom metadata to your JSON exports:

import gleam/dict

let metadata = dict.from_list([
  #("description", gleam_json.string("Social Network")),
  #("version", gleam_json.string("1.0")),
  #("tags", gleam_json.array(
    [gleam_json.string("social"), gleam_json.string("network")],
    of: fn(x) { x },
  )),
])

let options = json.JsonExportOptions(
  ..json.default_export_options(),
  metadata: option.Some(metadata),
)

let json_string = json.to_json(graph, options)

Generic Format Output

The default Generic format includes full metadata:

{
  "format": "yog-generic",
  "version": "2.0",
  "metadata": {
    "graph_type": "directed",
    "node_count": 2,
    "edge_count": 1
  },
  "nodes": [
    { "id": 1, "data": "Alice" },
    { "id": 2, "data": "Bob" }
  ],
  "edges": [
    { "source": 1, "target": 2, "data": "follows" }
  ]
}

Exporting DAGs (Directed Acyclic Graphs)

DAGs can be exported by first converting them to a regular graph:

import yog/dag/models as dag
import yog_io/json

// You have a DAG
let my_dag: dag.Dag(String, String) = ...

// Convert to Graph and export
let graph = dag.to_graph(my_dag)
let json_string = json.to_json(graph, json.default_export_options())

The output will include "graph_type": "directed" in the metadata. The acyclicity property is a semantic constraint that is preserved by the DAG type but not explicitly indicated in the JSON output.

MultiGraph Support

The JSON module now supports MultiGraphs (graphs with multiple parallel edges between nodes):

import yog/multi/model as multi
import yog_io/json

// Create a multigraph with parallel edges
let graph = multi.new(model.Directed)
  |> multi.add_node(1, "Alice")
  |> multi.add_node(2, "Bob")

let #(graph, _) = multi.add_edge(graph, from: 1, to: 2, with: "follows")
let #(graph, _) = multi.add_edge(graph, from: 1, to: 2, with: "mentions")
let #(graph, _) = multi.add_edge(graph, from: 1, to: 2, with: "likes")

// Export multigraph to JSON
let options = json.export_options_with(json.string, json.string)
let json_string = json.to_json_multi(graph, options)

All JSON format presets (Generic, D3Force, Cytoscape, VisJs, NetworkX) support multigraphs with unique edge IDs. The Generic and NetworkX formats include a "multigraph": true metadata flag.

See test/examples/multigraph_json_example.gleam for a complete example.

Module Overview

ModulePurpose
yog_ioConvenience functions for common operations
yog_io/tgfTGF (Trivial Graph Format) serialization and parsing
yog_io/ledaLEDA format with strict validation
yog_io/pajekPajek (.net) format for social network analysis
yog_io/jsonJSON format support with multiple presets and MultiGraph support
yog_io/graphmlFull GraphML support with custom mappers
yog_io/gdfFull GDF support with custom mappers

Format Support

TGF (Trivial Graph Format)

LEDA

Pajek

JSON

JSON module provides comprehensive support for common web-based graph formats with full bidirectional I/O.

GraphML

GDF

Format Compatibility Matrix

FormatDirectedUndirectedWeightedAttributesMultiGraphVisual
TGF
LEDA
Pajek
JSON (all)Partial
GraphML
GDF

All formats now support bidirectional read/write operations.

File Format Examples

GraphML Example

<?xml version="1.0" encoding="UTF-8"?>
<graphml xmlns="http://graphml.graphdrawing.org/xmlns">
  <key id="label" for="node" attr.name="label" attr.type="string"/>
  <key id="weight" for="edge" attr.name="weight" attr.type="string"/>
  <graph id="G" edgedefault="directed">
    <node id="1">
      <data key="label">Alice</data>
    </node>
    <node id="2">
      <data key="label">Bob</data>
    </node>
    <edge source="1" target="2">
      <data key="weight">friend</data>
    </edge>
  </graph>
</graphml>

GDF Example

nodedef>name VARCHAR,label VARCHAR
1,Alice
2,Bob
edgedef>node1 VARCHAR,node2 VARCHAR,directed BOOLEAN,label VARCHAR
1,2,true,friend

JSON Example (Generic Format)

{
  "format": "yog-generic",
  "version": "2.0",
  "metadata": {
    "graph_type": "directed",
    "node_count": 2,
    "edge_count": 1
  },
  "nodes": [
    { "id": 1, "data": "Alice" },
    { "id": 2, "data": "Bob" }
  ],
  "edges": [
    { "source": 1, "target": 2, "data": "friend" }
  ]
}

JSON Example (D3.js Format)

{
  "nodes": [
    { "id": "1" },
    { "id": "2" }
  ],
  "links": [
    { "source": "1", "target": "2", "value": "friend" }
  ]
}

Examples

Detailed examples demonstrating each format are located in the test/examples/ directory:

Running Examples Locally

The examples live in the test/examples/ directory and can be run directly:

gleam run -m examples/tgf_example
gleam run -m examples/leda_example
gleam run -m examples/pajek_example
gleam run -m examples/multigraph_json_example

Run all examples at once:

./run_examples.sh

Development

Running Tests

Run the full test suite:

gleam test

Run tests for a specific module:

./test_module.sh yog_io/json_test
./test_module.sh yog_io/graphml_test
./test_module.sh yog_io/tgf_test

Run a specific test function:

./test_module.sh yog_io/json_test to_json_generic_format_test

Property-Based Tests

In addition to traditional example-based tests, yog_io includes property-based tests using qcheck. These tests generate random graphs and verify roundtrip invariants:

# Run all tests (including property tests)
gleam test

# Run specific property test
gleam test yog_io@property_test.graphml_structural_roundtrip_property_test

Key Properties Verified:

PropertyDescription
Structural EqualityComplete graph topology preserved (GraphML, JSON)
Node CountNumber of nodes unchanged after roundtrip
Edge CountNumber of edges unchanged after roundtrip
Graph TypeDirected/Undirected property maintained
Undirected SymmetryFor undirected graphs, edge(u,v) implies edge(v,u)

Format Limitations:

Not all formats support complete structural equality:

See PROPERTY_TESTS.md for detailed documentation on invariants, hypotheses, and limitations.

Running Examples

Run all examples at once:

./run_examples.sh

Run a specific example:

gleam run -m examples/tgf_example
gleam run -m examples/multigraph_json_example

Building Documentation

gleam docs

Project Structure

Note: Example outputs (JSON files, GraphML files, etc.) are written to the output/ directory, which is ignored by git.

References

Format Specifications

Visualization Tools

Libraries

License

Apache-2.0

Search Document