API reference

Getting results out

What every chart does so that what it shows can leave the browser and still say what it is: a title, a subtitle and footnotes written with placeholders the chart fills from the view it draws, and one footnote the chart always writes last, saying when and by what the figure was drawn and what stands behind each statistic it printed. The rules are written once, in src/shared/titles.js, and every chart follows them; a page or a widget that writes the same words reaches them as BioViz.output.

Under the footnotes each chart offers three downloads: a PNG of the chart with its title and footnotes drawn in, the statistics R returned as CSV, and the table the chart drew from as CSV. And every chart writes its specification, its settings and filters as JSON data, from which BioViz.fromSpecification makes the same chart again: the format gsm.bio's batch runner reads (obot.roadmap#362). All of it is the requirement obot.roadmap#361.

At a glance

BioViz.groupComparison('#chart', {
  start_value: 'IL-6',
  visits: ['Week 4'],
  value_type: 'change',
  group_by: 'ARM',
  title: '{measure}: {value} at {visits}',
  subtitle: '{n} participants, by {group}',
  footnotes: ['Synthetic study from gsm.bio.', 'Filters: {filters}.']
}).init({ results, participants });

Above the chart:

> IL-6: Change from baseline at Week 4 > 186 participants, by Arm

Under it, the two footnotes and the chart's own:

> Synthetic study from gsm.bio. > Filters: none. > Drawn on 2026-10-04 by bio.viz 0.2.0. Statistics: Welch Two Sample t-test (Placebo n = 95, Treatment n = 91); computed by R in this browser.

The settings

Every chart has the three settings, with the same defaults and the same checks.

SettingDefaultWhat it is
titlenullThe title above the chart: text with placeholders. Null means none.
subtitlenullThe line under the title, written the same way. Null means none.
footnotesnullFootnotes under the chart: text, or a list of texts, with placeholders, in the order given. Empty texts are dropped. Null means none.

A title or a subtitle that is not text, or footnotes that are not text or a list of texts, are refused with a sentence that names the setting, as every other setting is. The chart's own footnote is not a setting: it is always written, and always last.

Placeholders

A placeholder is a name in braces: {measure}. When the chart draws, each one is replaced by the text of its value for the view drawn, and the title, the subtitle and the footnotes are written again whenever the chart draws again or an answer from R arrives.

Every chart fills these three:

PlaceholderWhat it holds
{filters}The filters in force, in words (Sex is F; Arm is Placebo), or none.
{date}The date drawn, in UTC, as ISO 8601: 2026-10-04.
{version}The bio.viz version, as the chart's own footnote says it.

Each chart adds its own, listed in its reference under Titles and footnotes: group comparison, association scatter, correlation matrix, biomarker screen, cross-tabulation and stratified survival. Every chart has {n}, the participants it draws.

Where they are drawn

The title and the subtitle are drawn at the top of the chart's own frame, above its toolbar, notes and figure; the footnotes at the bottom of it, under the figure and the statistics line and above the listing. They are part of the chart's element, so they move, wrap and print with it, and at a 390-pixel viewport they wrap like any other text, with no horizontal scroll. The title is a heading of level 2 to assistive technology.

The footnote the chart writes

The last footnote is the chart's, and it says three things:

  1. The date the chart was drawn, in UTC, and the bio.viz version that drew it: Drawn on 2026-10-04 by bio.viz 0.2.0. A build with changes made since its release says so, bio.viz 0.2.0 with development changes, so the footnote never names a release for code that is not one. The package's bioviz.development says which it is: true between a release and the preparation of the next, when the release log holds the package's version as released and another section is upcoming; false while a release is prepared, once its section is promoted, and in a tagged build. A unit test fails when the flag and the log disagree.
  2. For every statistic printed, every method R used, the counts R used, as R returned them, and every adjustment of its p-values (p-values adjusted by Holm, by Benjamini-Hochberg); with a pairwise test, the overall test first, Kruskal-Wallis rank sum test, with Wilcoxon rank sum test with continuity correction (…), p-values adjusted by Holm. Its form: Welch Two Sample t-test (Placebo n = 95, Treatment n = 91). One count is written n = 200; up to four groups each by name; more, such as a screen's biomarkers, as the least and the most with how many there are: n = 179 to 186 across 12 biomarkers.
  3. Which R computed them, as the connection says: computed by R in this browser for R started in the page; for a result stored with the page, computed by R 4.3.3 with gsm.bio 0.2.0 on 2026-10-01, stored with the page when the connection was told the versions and the date (computedBy, below), and stored with the page when it was not; for any other form, computed by R.

While an answer is on its way it says Statistics: waiting for R., and it is written again when the answer arrives. A chart that asked R nothing says No statistic was asked of R.; one whose answer did not come says that statistics are unavailable, or that R reported an error, as the statistics line under the chart says in full.

Nothing in the footnote is worked out by the chart. The method and the counts are R's, read off its answer; the chart only writes them down.

Stored results, and which R computed them

gsm.bio's widget records which R computed the results it stores with a page: computed_by, with r_version, gsm_bio_version and computed_at. Handed to the connection as computedBy, the record comes back with every stored answer, and the footnote names the two versions:

const connection = BioViz.r.createConnection({
  results: statistics.results,
  computedBy: statistics.computed_by // { r_version, gsm_bio_version, computed_at }
});

See the connection's reference.

Downloads

Every chart has the two settings, with the same defaults and the same checks.

SettingDefaultWhat it is
downloadstrueWhether the bar of downloads is shown under the footnotes.
png_scale2The PNG's resolution: image pixels per CSS pixel, from 1 to 4. At 2, 192 pixels to the inch.

The bar has three buttons. Each saves a file named for the chart and the view, the way safety.viz's kit saves its listing (a link to the file, clicked): bio.viz-{chart}-{view}.png, bio.viz-{chart}-{view}-statistics.csv and bio.viz-{chart}-{view}-table.csv, where the view is a few of the chart's placeholders in lower case joined by hyphens: bio.viz-cross-tab-arm-by-response.png. A chart's fileOf(kind) makes the same file without saving it, a promise of { name, blob }, for kind 'png', 'statistics' or 'table'.

The PNG

The chart's frame as the page draws it, from the title to the chart's own footnote: the title and subtitle, the notes, what the chart draws, the statistics line and the footnotes. What a reader works the chart with is left out: every element marked bv-no-picture, which the toolbar, the hint under the chart, the listing, the bar of downloads, a chart's own download buttons and the overview's pager buttons are. A chart's marks (the matrix's discs and key, the screen's zero line, intervals and dots, the bars and curves) are drawn at the size the page draws them, and text finds its own height. What scrolls sideways on the page, such as the survival chart's at-risk table on a phone, is drawn whole, and the picture is as wide as it needs to be.

It is drawn at png_scale image pixels per CSS pixel, so at the default it is twice the width the frame has on the page, and the file says so: its pHYs chunk gives the pixels per metre. Each Chart.js canvas is drawn again at that resolution for the picture, so the plotted marks are as sharp as the text. Its text chunks (iTXt, UTF-8) give its Title (the title and subtitle), its Description (the footnotes, one a line, the chart's own last) and its Software (the bio.viz version, as the footnote says it), so the file still says what it is when it is separated from the page.

When the picture cannot be made, the bar says so in a line of its own, where the reader sees it: a browser will not write a canvas larger than it allows (Safari about 16.7 million pixels, which a tall screen at png_scale 4 can pass), will not read one it has been given a picture from elsewhere, or cannot read the drawing. Another download, or a smaller png_scale, can follow.

The picture is the page's drawing, not a drawing for print: anything bound for a document should come from gsm.bio's static twin of the chart, which draws a vector figure from the same settings.

The statistics

The statistics R returned for the view drawn, as shown, as one table. For each answer there is a row for R's result (its part is result) and a row for each of its parts: each estimate (estimates), each row of a screen or a grid (rows), and so on, numbered by item. Every member R returned is a column, by its path: a nested member's names joined by a slash (counts/Placebo, so R's own dotted names, p.value, stay as they are), a list's entries by their place (data/variables/1/measure, the variable the grid's v1 stands for), and a list of values alone as one field, joined by | (R's notes may hold a ;). Each row also names the answer it came from (asked), the R function (function) and the data it was asked about (data/…, the identity a stored result is found by). A member of R's answer that would take one of those names, or two members written alike, is refused, not overwritten. Every number is R's, written as the shortest text that reads back in JavaScript as exactly the same number; R's own reader reads about one in fourteen such numbers one unit in the last place away. NaN, Inf and -Inf are written as R writes them, and a value missing as an empty field. Until R has answered there is nothing to download, and the button waits, saying so; a view whose statistics are unavailable says that instead; a view that asks R nothing, such as the group comparison's overview, offers none.

The table

The table the chart drew from, one row per participant drawn, with the headings the chart's listing uses: for the cross-tabulation the participant, the row and the column; for the group comparison the participant, the visit, the group and the value; and so on, as each chart's reference says. A group or a category that is a cut biomarker has the value it was cut from beside it (CRP at Baseline), so the cut can be made again from the file. A number is written as it was drawn, unrounded.

CSV

Every CSV file, the listing's export among them, is written by RFC 4180: records end in CRLF, and a field or a heading that holds a comma, a double quote, a carriage return or a line feed is written between double quotes with each double quote doubled. A heading that holds a comma is one heading (bio.viz#39). An empty field is a value the participant does not have; TRUE and FALSE are written as R reads them.

Values are written as they are, so a file reads back into R exactly. Two things follow for a spreadsheet:

Specifications

A chart's specification() returns what it draws as JSON data, and BioViz.fromSpecification(element, specification) makes the same chart from it. The tables are not in it: the chart made from it is given them with init, as any chart is.

const saved = JSON.stringify(chart.specification());
// later, or on another page, or in gsm.bio's batch runner:
BioViz.fromSpecification('#chart', saved, { connection }).init({ results, participants });
{
  "format": "bio.viz specification",
  "format_version": 1,
  "bio_viz_version": "0.2.0",
  "chart": "cross-tab",
  "settings": {
    "row_by": "ARM",
    "col_by": "RESPONSE",
    "percent": "row",
    "test": "chisq",
    "title": "{rows} by {columns}",
    "filters": [{ "value_col": "SEX", "label": "Sex" }],
    "…": "every other setting the chart has"
  },
  "filters": [{ "column": "SEX", "operator": "in", "values": ["F"] }]
}
MemberWhat it is
format"bio.viz specification": what the object is.
format_version1: the version of this format. A specification of another format version is refused, with a sentence that says which it is.
bio_viz_versionThe bio.viz version that wrote it: text, and needed. It is not compared: a specification from another version is read if what it holds is still what this version has (below).
chartThe chart: group-comparison, association-scatter, correlation-matrix, biomarker-screen, cross-tab or stratified-survival.
settingsEvery setting of the chart, by the names in its reference, as its controls now read: the biomarker, the visit, the groups, the test and so on, with its title, subtitle and footnotes. Left out, a setting keeps its default.
filtersEvery filter in force: { column, operator, values }, the column of the participant table it reads, "in", and the values it lets through. A filter at All is not listed; a filter of several values unticked to none is listed with no values, and lets nobody through.

The chart writes every setting it has, as the controls now read, so a chart made from its specification opens on the same view and writes the same specification again. The filters a chart offers are in its filters setting, each without where it starts; where each is now is in the specification's filters, and reading one lays it back onto its filter as where that filter starts.

Nothing is evaluated

A specification is data: text, numbers, true, false, null, lists and objects. It holds no function, expression or template that runs, and a title or a value that looks like code is text, filled and drawn as text. Two settings are the page's and never written: connection, the connection to R, and back, a way back; the page gives them again, as the third argument of fromSpecification. Anything else that is not data is refused.

What is refused

Each with a sentence that names what is wrong:

The filter rules

The value shapes

Every value is JSON data, in one of these shapes, as each chart's reference gives its settings:

ShapeWritten asFor
A columnits name, "ARM"row_by, group_by, color_by, every *_col
A column with a label{ "value_col": "ARM", "label": "Arm" }groups, filters, numbers, details
A biomarkerits name, "CRP"start_value, measure
A list of names["Week 4", "Week 8"]; [] is none, where a control can be emptied, null every onevisits, levels, biomarkers, measures
A variable{ "measure": "CRP", "visit": "Baseline", "value": "raw" } or { "col": "AGE" }x, y, with
A cut variablea variable with "cut": "median", "tertiles", "quartiles" or [2.5, 4]group_by, row_by, col_by, cuts
A choiceone of the words the setting liststest, percent, comparison, method, sort
A number20, 0limit, page, png_scale
Text"{rows} by {columns}"title, subtitle, footnotes

Which setting holds the biomarker each chart draws:

ChartThe biomarker
group-comparisonstart_value, at visits with value_type
association-scatterx and y, each a variable
correlation-matrixbiomarkers at visit, or measure at visits (mode)
biomarker-screenevery biomarker (measures), at visit with value_type
cross-tabrow_by or col_by, when either is a cut variable
stratified-survivalgroup_by, when it is a cut variable

Every setting's default is in the chart's reference and in the schema, as each setting's default: a setting left out of a specification takes it.

A list of specifications

Several specifications together are a JSON array of specification objects, each read on its own: [{ "format": "bio.viz specification", … }, …]. gsm.bio's batch runner reads such a list.

What the data cannot draw

A specification may ask for something the tables do not have: a grouping by a column they lack, a filter on one, a value a filter does not offer. The chart then draws what it can, as it would from settings, and says what it did not draw, in a line above it and in chart.notices, a list a caller such as gsm.bio's batch runner can read once the chart has drawn on its tables:

const chart = BioViz.fromSpecification('#chart', spec).init(tables);
chart.notices;
// [{ kind: 'setting', name: 'row_by', asked: 'NOPE', drawn: 'ARM',
//    said: 'Rows: NOPE is not in the tables, so the chart draws ARM.' },
//  { kind: 'filter', name: 'SEX', asked: ['X'], drawn: null,
//    said: 'Filter SEX: X is not one of its values, so it is at All.' }]

kind is setting or filter; name the setting or the column; asked what the specification asked for; drawn what the chart draws instead, null for none or All; said the sentence. A chart's own specification, read back on the same tables, has no notices.

What a specification holds of the view, and what it does not

It holds every setting as the controls read: the groupings, the visits, the test, the filters, the page of the group comparison's overview and of the screen (page), the screen's order (sort), and the cut variables the Rows, Columns and Groups controls offer (cuts), a line moved on the survival chart among them. Made from it on the same tables, a chart opens on the same view and writes the same specification again.

It does not hold what a reader does in passing, which ends when the chart draws again: a chart opened in place of another (a screen's row, a matrix's cell), the participants listed from a cell, a box, a region or a curve, the participant profile open beside it, and a cut line while it is being dragged.

Versions

format_version changes when a member of the format, or the meaning of a setting, changes; a specification of another format version is refused. Within one format version, a release of bio.viz that adds a setting gives it a default that keeps the old behaviour, so a specification written before it is read and draws as it did; one that holds a setting a release has removed is refused, naming the setting.

The schema's $id, https://jwildfire.github.io/bio.viz/schema/specification.json, resolves once a release with specifications is published from main; until then the file is at dev/schema/specification.json.

The schema

The format is a JSON Schema (2020-12), committed as src/data/specification.schema.json and published at schema/specification.json. It is written from each chart's own settings by node tools/write-specification-schema.mjs, so it names, for each chart, exactly the settings it has; the unit tests fail when the committed file is not what the charts make, and every specification the browser tests write is validated against it. gsm.bio's batch runner reads specifications by this schema.

fromSpecification(element, specification, page)

Makes the chart a specification names in an element, with its settings and filters. specification is the object or its JSON text; page holds what a specification never does, connection and back, and nothing else. Returns the chart; give it the tables with init.

The functions of BioViz.output

The rules above, for a page or a widget that writes the same words beside a chart.

fillText(template, values)

The template with every placeholder whose name is a key of values replaced by the text of its value, and every other left as written. A value that is null or undefined is written as nothing; anything else as String(value). Nothing is evaluated.

BioViz.output.fillText('{measure} at {visit}', { measure: 'CRP', visit: 'Week 4' });
// 'CRP at Week 4'
BioViz.output.fillText('${1 + 1} {unknown}', {});
// '${1 + 1} {unknown}'

fillParts(template, values)

The template filled, as its runs of text: [{ text, value }], each placeholder's value a run of its own (value: true), so a page can set values apart, as the charts do with <bdi>, without reading anything as markup.

placeholdersIn(template)

The names of the placeholders a template holds, each once, in the order written.

automaticFootnote(parts)

The footnote a chart writes last.

PartWhat it is
dateThe date drawn, 2026-10-04.
versionThe bio.viz version.
askedWhat the chart asked R, each { answer } with the answer as the connection gave it, or null while it is on its way. A chart's statistics() returns this.
ofWhat R's counts are of, when there are more than four: 'biomarkers'. Default 'groups'.

countsText(counts, of)

R's counts as the footnote writes them: a number as n = 200; an object of up to four groups as Placebo n = 95, Treatment n = 91; five or more as n = 179 to 186 across 12 biomarkers, with of naming what they are of. A count written as text that reads as a number is read as that number. Null when R returned none.

toCsv(rows, columns)

Rows as CSV by RFC 4180, with a heading row: columns is a list of { value_col, label }, which field of a row each column holds and its heading. Records end in CRLF.

parseCsv(text)

CSV read back by RFC 4180, every field as text: a list of records, the heading row first. The inverse of toCsv.

readSpecification(specification)

Reads a specification without making a chart, with every check fromSpecification makes: returns { chart, settings, version }, the chart's name, the settings it would be made with and the version that wrote it, or refuses with a sentence.

SPECIFICATION_FORMAT

What a specification says it is: bio.viz specification.

SPECIFICATION_VERSION

The version of the format this library writes and reads: 1.

FILTER_OPERATORS

The operators a filter may have: in, the values it lets through.

TITLE_DEFAULTS

The three settings' defaults, which every chart has: { title: null, subtitle: null, footnotes: null }.

What is not here

This page is the file docs/output.md, rendered. The site build fails when the library exports something the file does not document, or when the file documents a function the library does not export.