<!-- Source: https://docs.biosimulant.com/how-to/library/custom-visualizations -->

# How to Add Custom Visualizations

Modules can return visualization specs from `visualize()`. Labs Serve UI and platform consumers render these automatically after transport normalization.

## The visualization contract

Return either one visual spec or a list of them:

```python
def visualize(self):
    return {
        "render": "timeseries",
        "data": {...},
    }
```

## Timeseries

```python
def visualize(self):
    return {
        "render": "timeseries",
        "data": {
            "title": "Population Over Time",
            "xlabel": "Time",
            "ylabel": "Count",
            "series": [
                {"name": "Prey", "points": self.prey_history},
                {"name": "Predator", "points": self.predator_history},
            ],
        },
    }
```

## Bar chart

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

## Table

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

## Image

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

## 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"]])
```

If the visualisation is a separate downstream module, its inputs are not visible
until after the producer's outputs have been committed. Run a final settle turn
before collecting visuals:

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

In `lab.yaml`, use `runtime.settle_steps: 1` for the same direct
producer-to-visualisation pattern.

## Tips

- Keep timeseries histories bounded or downsampled for long runs.
- Use units in labels where possible.
- Return JSON-serializable data or artifact-backed image outputs.
- Keep lab visualisation modules lab-local when distributing portable `.bsilab`
  archives. Shared repository copies can be checked for drift, but labs should
  not depend on a shared runtime visualisation package unless portability is no
  longer a requirement.

## Next steps

- [Serve a Local Lab](/how-to/library/serve-local-lab)
- [Visualization Contract Reference](/references/library/visualization)
