<!-- Source: https://docs.biosimulant.com/references/visualization -->

# Visualization Contract

Modules implement `visualize()` to return one visual spec or a list of them.
Labs Serve UI and the platform render these specs after transport normalization.

## Collect visuals from a world

```python
world.run(duration=10.0)
visuals = world.collect_visuals()

for entry in visuals:
    print(entry["module"], [v["render"] for v in entry["visuals"]])
```

## Visuals built by a downstream module

A module that plots another module's outputs only sees them after they are
committed at a boundary. Give that module `ExecutionPolicy.ONCE_AFTER_RUN`.
BioWorld runs it once after the final window, with the final committed inputs.

```python

class PopulationTable(biosim.BioModule):
    execution_policy = biosim.ExecutionPolicy.ONCE_AFTER_RUN

    def __init__(self):
        self.final_count = None

    def inputs(self):
        return {"population_state": biosim.SignalSpec.record(schema={"count": "int64"})}

    def execute(self, inputs, *, context):
        state = inputs.get("population_state")
        if state is not None:
            self.final_count = state.value["count"]
        return {}

    def visualize(self):
        return {
            "render": "table",
            "data": {
                "columns": ["Metric", "Value"],
                "rows": [["Final count", str(self.final_count)]],
            },
        }
```

Do not use `world.settle()` or `runtime.settle_steps` for this. Settling skips
modules that implement `execute()` and only calls modules that implement
`advance_window()`.

## Spec fields

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `render` | `str` | Yes | Visual type. Known types are `"timeseries"`, `"bar"`, `"graph"`, `"table"`, `"image"`, `"text"`, and `"structure3d"`, plus `"custom:<name>"` for your own renderer. Validation only requires a non-empty string. |
| `data` | `dict` | Yes | Render-type-specific, JSON-serializable content |
| `description` | `str` | No | Optional text description |

For `structure3d` file sources, see [Artifact Outputs](/references/artifact-outputs).

## Timeseries

```python
def visualize(self):
    return {
        "render": "timeseries",
        "data": {
            "title": "Membrane Potential",
            "xlabel": "Time (ms)",
            "ylabel": "Voltage (mV)",
            "series": [
                {"name": "Neuron 1", "points": self.v1_history},
                {"name": "Neuron 2", "points": self.v2_history}
            ]
        }
    }
```

| Field | Type | Description |
|-------|------|-------------|
| `title` | `str` | Chart title |
| `xlabel` | `str` | X-axis label |
| `ylabel` | `str` | Y-axis label |
| `series` | `list[{name, points}]` | Named data series |

Each `points` entry is a list of `[x, y]` pairs: `[[0.0, -65.0], [0.1, -60.2], ...]`.

## Bar Chart

```python
def visualize(self):
    return {
        "render": "bar",
        "data": {
            "title": "Spike Counts",
            "items": [
                {"label": "Excitatory", "value": self.exc_count},
                {"label": "Inhibitory", "value": self.inh_count}
            ]
        }
    }
```

| Field | Type | Description |
|-------|------|-------------|
| `title` | `str` | Chart title |
| `items` | `list[{label, value}]` | Labeled values |

## Table

```python
def visualize(self):
    return {
        "render": "table",
        "data": {
            "columns": ["Metric", "Value"],
            "rows": [
                ["Mean Rate", f"{self.mean_rate:.2f} Hz"],
                ["Total Spikes", str(self.total_spikes)]
            ]
        }
    }
```

| Field | Type | Description |
|-------|------|-------------|
| `columns` | `list[str]` | Column headers |
| `rows` | `list[list[str]]` | Row data (strings) |

## Image (Raster Plot, Heatmap, etc.)

For complex visualizations generated with matplotlib or similar:

```python
def visualize(self):
    return {
        "render": "image",
        "data": {
            "src": self.raster_image_base64,  # data:image/png;base64,... or raw base64
            "alt": "Spike Raster",
            "width": 800,
            "height": 400
        }
    }
```

| Field | Type | Description |
|-------|------|-------------|
| `src` | `str` | Base64-encoded image (PNG/JPEG) |
| `alt` | `str` | Alt text |
| `width` | `int` | Display width in pixels |
| `height` | `int` | Display height in pixels |

## Tips

- Keep timeseries histories bounded or downsampled for long runs.
- Put units in axis labels.
- Keep lab visualisation modules inside the lab when you distribute portable
  `.bsilab` archives. A lab that imports a shared visualisation package is no
  longer self-contained.

## See Also

- [BioModule API](/references/biomodule-api): implementing `visualize()`
- [Artifact Outputs](/references/artifact-outputs): durable file outputs for structures and generated files
- [Labs Serve UI Reference](/references/labs-serve): rendering visualizations in the local lab UI
