---
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: 17
updated: 2026-10-08
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 a comparison or a trend for numbers that do not compare. Three unrelated stories that each mention one figure are not a trend. A figure is still worth showing. Most weekly pulls have one value per `briefing.stories[].chart_series` entry, and that is the normal case.

For that case build a **figures panel**: 3 to 6 cards. Each card holds the value, its `unit`, the `qualifier` ("at least" for `at_least`), the `claim_text`, the item number and the source link. Take `parse_confidence: high` series first, one per story. Draw no bars, no ranking and no common scale across different units or measures.

Title the panel for what it is ("Figures from this week's items"), and label it "Created by your agent from this pull". Use a bar chart only when two or more items report the same measure in the same unit and period.

If your host cannot make images, say so in one line and give the same figures as a Markdown table.

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 Manifests. Full output (the default) carries all `facts` per Brief at `brief.body.facts`; compact output (`compact: true`) carries up to three at `brief.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. A fact can also state its unit in a separate `unit` field and its time span in a `period` field. The helper keeps a label only if two or more sources report it in the same unit and for the same period. The [reference formatter](/agent-guides/present-manifests-to-your-users) applies the same rule in `comparison_table`, and also counts one article from several Streams as one source:

```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"))
            stated = fact.get("unit") or ""  # a fact can carry its unit apart from its value
            if not parsed or not fact.get("label") or (parsed[1] and stated and parsed[1] != stated):
                continue
            by_label.setdefault(fact["label"], []).append(
                {
                    "source": source,
                    "date": m.get("published_date"),
                    "value": parsed[0],
                    "unit": parsed[1] or stated,
                    "period": fact.get("period") or "",
                    "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
        and len({r["period"] for r in rows}) == 1
    }
```

A comparison table works on every display:

```python
def with_unit(value, unit):
    number = format(value, ",g")
    if unit in ("$", "€", "£"):
        return unit + number
    return number + unit if unit in ("", "%") else number + " " + unit


def markdown_table(label, rows):
    period = rows[0].get("period")
    lines = ["| Source | Date | " + label + (" (" + period + ")" if period else "") + " |", "| --- | --- | ---: |"]
    for r in rows:
        lines.append("| " + r["source"] + " | " + str(r["date"]) + " | " + with_unit(r["value"], r["unit"]) + " |")
    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
    desc = "; ".join(r["source"] + ": " + with_unit(r["value"], r["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 = with_unit(r["value"], r["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.

## Failure modes

| Symptom | Cause | What to do |
| --- | --- | --- |
| `comparable_facts` returns nothing | Facts are text, units or periods 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 cannot draw, say so in one line and show the panel as a table. If the numbers do not compare, say that and skip the comparison. This care builds trust in the visuals you do draw.

## Next guide

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