---
title: Charts, tables and infographics
category: read
summary: When the numbers in a Manifest deserve a picture, how to build a table, chart or infographic from them, and how to stay honest about what the data supports.
order: 15
updated: 2026-10-07
next: build-and-save-a-beacon
related: present-manifests-to-your-users, manifest-anatomy-field-guide
reference: /agents/personal-agent-onboarding.txt, /synorb-instructions.md
---

A good visual lets your user see in two seconds what a paragraph needs a minute to say. A bad visual invents precision. The method below lets the visual show only numbers that Synorb delivered.

## When to use this

Draw something when the story has numbers that **move or compare**:

- a change over time (a rate path, a price, a count rising day by day),
- a comparison across sources or entities (three banks' rate views side by side),
- a before and after (old pricing against new, last quarter against this one), or
- a result overview your user will scan (a run's hits by Stream, the window covered, allowance left).

Do not draw anything for a story with no comparable numbers. Three unrelated stories that each mention one figure are not a trend. A clear narrative is enough, and it is better than a decoration.

Pick the richest form your display can show:

| Your display can | Use |
| --- | --- |
| Render HTML, SVG or images (an artifact, a dashboard, a web view) | An SVG chart, stat card or full infographic, as below |
| Run code (a notebook, a sandbox) | Any plotting library you have, saved as an image with alt text |
| Show only Markdown | A table, plus a text bar if it helps |
| Speak only (voice) | Lead with the one number that matters, in a sentence |
| Send email | An inline table and, if you attach an image, alt text and a link to the sources |

## Smallest working call

Pull a compact scan. Compact output carries up to three `facts` per Brief at `brief.facts`. Full bodies (`compact: false`) carry all of them at `brief.body.facts`. Each fact is a `label` and `value` pair traceable to the source:

```mcp billed
synorb-manifests
{"stream_ids": ["STREAM_ID"], "days": 14, "target_count": 10, "compact": true}
```

Now find the numbers that are truly comparable. This helper accepts a figure only if it is a plain number with an optional currency symbol or percent sign. It keeps a label only if two or more sources report it in the same unit:

```python
import re
from html import escape

NUMBER = re.compile(r"^\s*([$€£]?)\s*(-?\d[\d,]*\.?\d*)\s*(%?)\s*$")


def parse_number(value):
    match = NUMBER.match(str(value))
    if not match:
        return None
    unit = match.group(1) or match.group(3)
    return float(match.group(2).replace(",", "")), unit


def comparable_facts(manifests, min_sources=2):
    by_label = {}
    for m in manifests:
        brief = m.get("brief") or {}
        facts = brief.get("facts") or (brief.get("body") or {}).get("facts") or []
        source = (m.get("source") or {}).get("source_name") or "Source"
        for fact in facts:
            parsed = parse_number(fact.get("value"))
            if parsed and fact.get("label"):
                by_label.setdefault(fact["label"], []).append(
                    {
                        "source": source,
                        "date": m.get("published_date"),
                        "value": parsed[0],
                        "unit": parsed[1],
                        "manifest_id": m.get("manifest_id"),
                    }
                )
    return {
        label: rows
        for label, rows in by_label.items()
        if len(rows) >= min_sources and len({r["unit"] for r in rows}) == 1
    }
```

A comparison table works on every display:

```python
def markdown_table(label, rows):
    unit = rows[0]["unit"]
    lines = ["| Source | Date | " + label + " |", "| --- | --- | ---: |"]
    for r in rows:
        shown = unit + format(r["value"], ",g") if unit == "$" else format(r["value"], ",g") + unit
        lines.append("| " + r["source"] + " | " + str(r["date"]) + " | " + shown + " |")
    return "\n".join(lines)
```

A bar chart is a self-contained SVG for any display that can render one. It has a title, a text description for screen readers, a value on every bar, and a source line:

```python
def svg_bars(title, rows, source_note=""):
    if not rows or any(r["value"] < 0 for r in rows):
        return None  # use a table for negative values
    width, left, bar_h, gap, top = 560, 190, 26, 10, 46
    height = top + len(rows) * (bar_h + gap) + 34
    peak = max(r["value"] for r in rows) or 1
    unit = rows[0]["unit"]
    desc = "; ".join(r["source"] + ": " + format(r["value"], ",g") + unit for r in rows)
    parts = [
        '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ' + str(width) + " " + str(height) + '" role="img" aria-labelledby="t d">',
        '<title id="t">' + escape(title) + "</title>",
        '<desc id="d">' + escape(desc) + "</desc>",
        '<text x="0" y="24" font-size="18" font-weight="600" fill="currentColor">' + escape(title) + "</text>",
    ]
    for i, r in enumerate(rows):
        y = top + i * (bar_h + gap)
        w = max(2, (width - left - 80) * r["value"] / peak)
        label = format(r["value"], ",g") + unit
        parts.append('<text x="0" y="' + str(y + 18) + '" font-size="13" fill="currentColor">' + escape(r["source"]) + "</text>")
        parts.append('<rect x="' + str(left) + '" y="' + str(y) + '" width="' + format(w, ".1f") + '" height="' + str(bar_h) + '" rx="3" fill="#2d7864"/>')
        parts.append('<text x="' + format(left + w + 8, ".1f") + '" y="' + str(y + 18) + '" font-size="13" fill="currentColor">' + escape(label) + "</text>")
    parts.append('<text x="0" y="' + str(height - 8) + '" font-size="11" fill="currentColor" opacity="0.7">' + escape(source_note) + "</text>")
    parts.append("</svg>")
    return "\n".join(parts)
```

Use them together:

```python
facts = comparable_facts(manifests)
for label, rows in facts.items():
    print(markdown_table(label, rows))
    svg = svg_bars(label, rows, "Sources: " + ", ".join(sorted({r["source"] for r in rows})))
```

## What you get back

For three sources that each report the same measure in percent, `markdown_table` returns:

```markdown
| Source | Date | Policy rate view |
| --- | --- | ---: |
| Bank A | 2026-10-02 | 4.25% |
| Bank B | 2026-10-03 | 4.5% |
| Bank C | 2026-10-05 | 4.75% |
```

`svg_bars` returns one `<svg>` string with those three bars. Show it, add one sentence of reading ("Bank C is the most hawkish of the three"), and link each source. The rate figures above are examples. Your figures come from the Briefs you pulled.

### Rules that keep a visual honest

1. **Chart only numbers that appear in a Brief's `facts`, in a Signal statement, or in the cited source.** Never estimate, interpolate or invent a point to complete a series.
2. **Label every visual with its source and date.** Link the sources next to it.
3. **Match labels exactly.** If two sources word the same measure differently, say so in a table row. Do not merge them without telling your user.
4. **State what the picture shows in the title,** for example "Three banks' rate views, week of Oct 2". Do not use "Chart" as a title.
5. **Give it alt text.** Do not depend on color alone. Print the value on the mark.
6. **Check that the field exists.** `render.also_suitable` is a hint from compact Brief facts. It can be empty on full-body and REST pulls. Read the facts yourself. Build a shape only if the numbers support it.

### Composing an infographic

For a single story with several numbers, assemble a card in this order:

1. A headline sentence that states the finding.
2. One large number with its label.
3. Two or three supporting numbers.
4. One chart.
5. A source line with the date and links.

Build each piece from the helpers above. Keep one idea per card.

A `timeline` of dated events (`brief.timeline` in compact output, `brief.body.timeline` in full output) can be a dated list or a horizontal line of events. The same rule applies: draw only the dates the source gave.

Advanced-profile MCP clients can also call `synorb-studio-render` to make a deterministic Markdown report from a Studio template. They call `synorb-studio-artifact` to read the result. A render costs the same as an equivalent Manifest pull, so use it with care.

## Failure modes

| Symptom | Cause | What to do |
| --- | --- | --- |
| `comparable_facts` returns nothing | Facts are text, units differ, or only one source reports the measure | Use prose or a table of the text facts. Do not force a chart. |
| A value parses wrong, such as "1.2B" | The helper reads only plain numbers, percents and currency symbols | Read the value from the Claim text yourself. Keep its unit in the label. Chart it only if every row has the same unit. |
| Negative values | `svg_bars` declines them | Use a table. |
| The SVG does not render | The display cannot render SVG | Use the table. |
| Facts are missing from a Manifest | The Brief did not include them | Skip the visual. Do not invent facts. |
| Different dates make a bad comparison | Sources reported on different days | Show the date column. Say "as of" in your sentence. |

## Make it good for your user

One clear visual is better than three complex ones. Put the finding in the title and keep the chart small. Let the sentence around it say what it means for your user. Offer the link to the source. If you chose not to draw a visual because the numbers were not comparable, say so. This care builds trust in the visuals you do draw.

## Next guide

[Build and save a Beacon](/agent-resources/build-and-save-a-beacon): turn what your user cares about into a saved, reusable watch.
