Skip to content

solvi.charts

Verified charts (preview): a text with numbers → an SVG in which every number is quoted from the text; the proposers, the checker, the renderer.

Verified charts: a text with numbers → a chart in which every number is quoted from the text.

from solvi.charts import chart, RuleProposer, LLMProposer
run = chart(text, "revenue by region")                 # the rule-based proposer by default
run = chart(text, proposer=LLMProposer("http://127.0.0.1:8080/v1", "qwen2.5-7b-instruct"))
open("chart.svg", "w").write(run.output)               # None when nothing verified
print(run.report())                                     # kept / dropped / changed, and why

The first specialist of solvi.specialist: a proposer writes a ChartSpec (a chart type, series of labelled values, each value with its quote in the text, a unit, a scale, a title); the checker verifies every value — the quote is in the text, it holds that number (thousands separators, decimals, "4.2 billion", "15%", "1 500 000 руб" are read; an ambiguous "1.000" or "3 100" is not), with the chart's unit (percent is not percentage points, dollars are not euros, "480 employees" is not 480 "stores") and scale — and the chart type against the data (a pie only for shares that add up to 100% or to a stated total, a line needs two points, one unit per chart). A value that does not verify is not drawn: the report says what was dropped and why, and the chart marks the gap. The renderer draws a deterministic, accessible SVG from the verified values only, and the trace replays: the same checks, the same bytes.

ChartChecker

ChartChecker(decimal=None, pie_tolerance=None)

Checks a ChartSpec against its source. decimal: "." or "," when the source's locale says which one is the decimal separator (else "1.000" is ambiguous and dropped).

FixedProposer

FixedProposer(spec, id='fixed')

Returns the spec it was given (a ChartSpec, a dict or JSON): a stand-in for a model in tests and examples.

LLMProposer

LLMProposer(base_url, model, api_key=None, *, timeout=60.0, max_tokens=1500, json_mode=True, opener=None, retries=2, backoff=1.0, sleep=None)

A ChartSpec from any OpenAI-compatible POST {base_url}/chat/completions, over the shared client of solvi.remote (retries with backoff; a wrong key, model or URL raises solvi.llm.LLMError; no answer raises solvi.remote.NoAnswer). The API key is sent in the Authorization header only, never recorded. opener: a replacement for urllib's urlopen (tests).

RuleProposer

RuleProposer(unit=None, kind=None, title=None)

A rule-based proposer: every readable number of the chosen unit, labelled with the words before it in its clause.

unit: the unit to chart ("%", "USD", "employees" …); None: the unit most numbers share (with a question, the one whose sentences share most words with it). kind: "bar" / "line" / "pie"; None: pie for shares that add up to 100%, line for time labels (years, quarters, months), bars otherwise. A number whose label says "total" becomes the spec's total.

Drawing dataclass

Drawing(svg: str, width: int, height: float, texts: list = list(), fonts: set = set(), notes: list = list())

The SVG and what the layout solver placed: every text box (for the no-overlap and in-canvas checks), the fonts used and notes (a label it could not place: it is in the description).

ChartSpec

Bases: BaseModel

kind: bar | line | pie. unit: "%", a currency ("USD", "$", "EUR", "₽" …), "pp", a word ("tonnes", "users") or "" for plain counts. scale: the values are in thousands / millions / billions of the unit ("" = as is). total: a total the source states for the values (checked like any value; the parts must add up to it).

Point

Bases: BaseModel

One value: a category label, the number (in the chart's scale) and the quote that states it.

SourceQuote

Bases: BaseModel

A passage of the source, copied character for character; start is its offset (optional: without it the checker looks the passage up, and one that occurs several times — as whole words and numbers — must verify at every such place, else the point is dropped: give start).

VerifiedChart

Bases: BaseModel

The only input of the renderer: every number in it is in the source at [start, end).

VerifiedPoint

Bases: BaseModel

A value that verified: the number (in the chart's scale), where the source states it and how it is written there.

ChartSpecialist

ChartSpecialist(proposer=None, *, decimal=None)

Bases: Specialist

Text → verified chart (see the module docs). decimal: "." or "," when the text's locale says which one is the decimal separator.

draw

draw(checked: Checked) -> Drawing

The Drawing (the SVG with its layout: text boxes, fonts, notes) of a check's verified chart.

canon_unit

canon_unit(u)

A unit as written → its canonical form: "USD" / "EUR" / … for currencies, "%" for percent, "pp" for percentage points, otherwise the lower-cased word without a plural "s" ("tonnes" → "tonne"), "" for none.

spec_json_schema

spec_json_schema()

The JSON schema of ChartSpec (for a server that takes a json_schema response format).

contrast

contrast(a, b)

The WCAG contrast ratio of two colours (#rrggbb).

render_svg

render_svg(chart: VerifiedChart, proposed=None) -> Drawing

A VerifiedChart → Drawing (the SVG and its layout). proposed: how many values were proposed (the footer says how many of them verified). A chart the checker never produces — no categories, no verified value, a pie whose values do not add up to more than zero or hold a negative — raises ValueError (it cannot be drawn honestly).

chart

chart(text, question=None, *, proposer=None, decimal=None)

A text (and a question) → Run: .output (the SVG, or None), .report(), .issues, .trace.

ChartSpec: the typed intermediate a proposer writes — a chart type, series of labelled values, and for every value the quote in the source it was read from.

SourceQuote

Bases: BaseModel

A passage of the source, copied character for character; start is its offset (optional: without it the checker looks the passage up, and one that occurs several times — as whole words and numbers — must verify at every such place, else the point is dropped: give start).

Point

Bases: BaseModel

One value: a category label, the number (in the chart's scale) and the quote that states it.

ChartSpec

Bases: BaseModel

kind: bar | line | pie. unit: "%", a currency ("USD", "$", "EUR", "₽" …), "pp", a word ("tonnes", "users") or "" for plain counts. scale: the values are in thousands / millions / billions of the unit ("" = as is). total: a total the source states for the values (checked like any value; the parts must add up to it).

VerifiedPoint

Bases: BaseModel

A value that verified: the number (in the chart's scale), where the source states it and how it is written there.

VerifiedChart

Bases: BaseModel

The only input of the renderer: every number in it is in the source at [start, end).

The chart checker: every number of a ChartSpec against the source, then the chart type against the data. Deterministic; what does not verify is dropped or changed with a reason, never repaired by a guess.

Reading dataclass

Reading(start: int, end: int, as_written: str, value: Decimal | None, unit: str, words: tuple, ambiguous: str = '')

A number as the source states it: its absolute value (scale applied), its unit and where it is.

SourceIndex

SourceIndex(source, decimal=None)

Every number candidate of a source, read once.

in_quote

in_quote(qs, qe)

The readings whose digits lie inside [qs, qe).

ChartChecker

ChartChecker(decimal=None, pie_tolerance=None)

Checks a ChartSpec against its source. decimal: "." or "," when the source's locale says which one is the decimal separator (else "1.000" is ambiguous and dropped).

canon_unit

canon_unit(u)

A unit as written → its canonical form: "USD" / "EUR" / … for currencies, "%" for percent, "pp" for percentage points, otherwise the lower-cased word without a plural "s" ("tonnes" → "tonne"), "" for none.

Proposers of a ChartSpec. Any callable (source, question) -> ChartSpec | dict is one; these are three:

RuleProposer — rule-based, no model: numbers of one unit and the words before them as labels (tests, simple texts); LLMProposer — any OpenAI-compatible chat-completions server (standard library HTTP), asked for the spec as JSON; FixedProposer — returns a spec given in advance (a stand-in for a model, a hand-written spec, a recorded proposal).

A proposer is never trusted: the checker verifies every number it writes against the source.

FixedProposer

FixedProposer(spec, id='fixed')

Returns the spec it was given (a ChartSpec, a dict or JSON): a stand-in for a model in tests and examples.

RuleProposer

RuleProposer(unit=None, kind=None, title=None)

A rule-based proposer: every readable number of the chosen unit, labelled with the words before it in its clause.

unit: the unit to chart ("%", "USD", "employees" …); None: the unit most numbers share (with a question, the one whose sentences share most words with it). kind: "bar" / "line" / "pie"; None: pie for shares that add up to 100%, line for time labels (years, quarters, months), bars otherwise. A number whose label says "total" becomes the spec's total.

LLMProposer

LLMProposer(base_url, model, api_key=None, *, timeout=60.0, max_tokens=1500, json_mode=True, opener=None, retries=2, backoff=1.0, sleep=None)

A ChartSpec from any OpenAI-compatible POST {base_url}/chat/completions, over the shared client of solvi.remote (retries with backoff; a wrong key, model or URL raises solvi.llm.LLMError; no answer raises solvi.remote.NoAnswer). The API key is sent in the Authorization header only, never recorded. opener: a replacement for urllib's urlopen (tests).

spec_json_schema

spec_json_schema()

The JSON schema of ChartSpec (for a server that takes a json_schema response format).

The chart renderer: a VerifiedChart → SVG bytes, deterministic (no clock, no randomness, fixed number formatting), no dependencies. Accessible: and <desc> (the data as text), a <title> on every mark, direct value labels instead of an axis (so the only numbers drawn are the verified ones), text at 12 px or more, colours at 3:1 or more against the background and text at 4.5:1 or more. A small layout solver keeps text from overlapping: wrapped titles and labels, vertical bars that turn horizontal when their labels do not fit, candidate positions for line labels, pushed-apart pie labels with leader lines. Text width is estimated from a per-character table (no font files): the estimate is deliberately wide.</p> <div class="doc doc-children"> <div class="doc doc-object doc-class"> <h2 id="solvi.charts.render.Drawing" class="doc doc-heading"> <span class="doc doc-object-name doc-class-name">Drawing</span> <span class="doc doc-labels"> <small class="doc doc-label doc-label-dataclass"><code>dataclass</code></small> </span> <a href="#solvi.charts.render.Drawing" class="headerlink" title="Permanent link">¶</a></h2> <div class="doc-signature highlight"><pre><span></span><code><a id="__codelineno-0-1" name="__codelineno-0-1" href="#__codelineno-0-1"></a><span class="nf">Drawing</span><span class="p">(</span><span class="n">svg</span><span class="p">:</span> <span class="nb">str</span><span class="p">,</span> <span class="n">width</span><span class="p">:</span> <span class="nb">int</span><span class="p">,</span> <span class="n">height</span><span class="p">:</span> <span class="nb">float</span><span class="p">,</span> <span class="n">texts</span><span class="p">:</span> <span class="nb">list</span> <span class="o">=</span> <span class="nb">list</span><span class="p">(),</span> <span class="n">fonts</span><span class="p">:</span> <span class="nb">set</span> <span class="o">=</span> <span class="nb">set</span><span class="p">(),</span> <span class="n">notes</span><span class="p">:</span> <span class="nb">list</span> <span class="o">=</span> <span class="nb">list</span><span class="p">())</span> </code></pre></div> <div class="doc doc-contents "> <p>The SVG and what the layout solver placed: every text box (for the no-overlap and in-canvas checks), the fonts used and notes (a label it could not place: it is in the description).</p> <div class="doc doc-children"> </div> </div> </div> <div class="doc doc-object doc-function"> <h2 id="solvi.charts.render.contrast" class="doc doc-heading"> <span class="doc doc-object-name doc-function-name">contrast</span> <a href="#solvi.charts.render.contrast" class="headerlink" title="Permanent link">¶</a></h2> <div class="doc-signature highlight"><pre><span></span><code><a id="__codelineno-0-1" name="__codelineno-0-1" href="#__codelineno-0-1"></a><span class="nf">contrast</span><span class="p">(</span><span class="n">a</span><span class="p">,</span> <span class="n">b</span><span class="p">)</span> </code></pre></div> <div class="doc doc-contents "> <p>The WCAG contrast ratio of two colours (#rrggbb).</p> </div> </div> <div class="doc doc-object doc-function"> <h2 id="solvi.charts.render.render_svg" class="doc doc-heading"> <span class="doc doc-object-name doc-function-name">render_svg</span> <a href="#solvi.charts.render.render_svg" class="headerlink" title="Permanent link">¶</a></h2> <div class="doc-signature highlight"><pre><span></span><code><a id="__codelineno-0-1" name="__codelineno-0-1" href="#__codelineno-0-1"></a><span class="nf">render_svg</span><span class="p">(</span><span class="n">chart</span><span class="p">:</span> <span class="n">VerifiedChart</span><span class="p">,</span> <span class="n">proposed</span><span class="o">=</span><span class="kc">None</span><span class="p">)</span> <span class="o">-></span> <span class="n">Drawing</span> </code></pre></div> <div class="doc doc-contents "> <p>A VerifiedChart → Drawing (the SVG and its layout). proposed: how many values were proposed (the footer says how many of them verified). A chart the checker never produces — no categories, no verified value, a pie whose values do not add up to more than zero or hold a negative — raises ValueError (it cannot be drawn honestly).</p> </div> </div> </div> </div> </div> </article> </div> <script>var target=document.getElementById(location.hash.slice(1));target&&target.name&&(target.checked=target.name.startsWith("__tabbed_"))</script> </div> <button type="button" class="md-top md-icon" data-md-component="top" hidden> <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M13 20h-2V8l-5.5 5.5-1.42-1.42L12 4.16l7.92 7.92-1.42 1.42L13 8z"/></svg> Back to top </button> </main> <footer class="md-footer"> <nav class="md-footer__inner md-grid" aria-label="Footer" > <a href="../specialist/" class="md-footer__link md-footer__link--prev" aria-label="Previous: solvi.specialist"> <div class="md-footer__button md-icon"> <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M20 11v2H8l5.5 5.5-1.42 1.42L4.16 12l7.92-7.92L13.5 5.5 8 11z"/></svg> </div> <div class="md-footer__title"> <span class="md-footer__direction"> Previous </span> <div class="md-ellipsis"> solvi.specialist </div> </div> </a> <a href="../longdoc/" class="md-footer__link md-footer__link--next" aria-label="Next: solvi.longdoc"> <div class="md-footer__title"> <span class="md-footer__direction"> Next </span> <div class="md-ellipsis"> solvi.longdoc </div> </div> <div class="md-footer__button md-icon"> <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M4 11v2h12l-5.5 5.5 1.42 1.42L19.84 12l-7.92-7.92L10.5 5.5 16 11z"/></svg> </div> </a> </nav> <div class="md-footer-meta md-typeset"> <div class="md-footer-meta__inner md-grid"> <div class="md-copyright"> <div class="md-copyright__highlight"> Apache-2.0 </div> Made with <a href="https://squidfunk.github.io/mkdocs-material/" target="_blank" rel="noopener"> Material for MkDocs </a> </div> </div> </div> </footer> </div> <div class="md-dialog" data-md-component="dialog"> <div class="md-dialog__inner md-typeset"></div> </div> <script id="__config" type="application/json">{"annotate": null, "base": "../..", "features": ["navigation.sections", "navigation.indexes", "navigation.top", "navigation.footer", "toc.follow", "search.suggest", "search.highlight", "content.code.copy"], "search": "../../assets/javascripts/workers/search.2c215733.min.js", "tags": null, "translations": {"clipboard.copied": "Copied to clipboard", "clipboard.copy": "Copy to clipboard", "search.result.more.one": "1 more on this page", "search.result.more.other": "# more on this page", "search.result.none": "No matching documents", "search.result.one": "1 matching document", "search.result.other": "# matching documents", "search.result.placeholder": "Type to start searching", "search.result.term.missing": "Missing", "select.version": "Select version"}, "version": null}</script> <script src="../../assets/javascripts/bundle.d7400e89.min.js"></script> </body> </html>