library(rtemis.draw)
Attaching package: 'rtemis.draw'
The following object is masked from 'package:graphics':
Axis
Attaching package: 'rtemis.draw'
The following object is masked from 'package:graphics':
Axis
A chart config describes a chart as a document: which columns it binds, its semantics, its appearance. It holds no data and no rendering targets, it serializes to JSON, and it validates against a published schema. It is how a chart is defined in one place and drawn in another.
draw_scatter(x, y, ...) ---------------------------> option -> widget
draw(ScatterConfig, data = df) --compile()--> option -> widget
Two rules decide what belongs in a config:
dat_path read at draw time.theme, width, height, element_id, filename — are arguments to draw(), not properties. That is what lets one document render correctly in an IDE pane and in a web app, adapting its presentation while keeping its meaning.Every chart type has a setup_*Config() constructor. Arguments are column names where the chart binds data, and values everywhere else:
<ScatterConfig>
type: <chr> scatter
dat_path: <NUL> NULL
title: <chr> Penguin bill and flipper
origin: <chr> default, user, user, user, ...
writer: <NUL> NULL
x: <chr> bill_len
y: <chr> flipper_len
size: <NUL> NULL
group: <chr> species
fit: <chr> gam
se: <lgc> TRUE
n_fit: <int> 200
fit_alpha: <nmr> 0.25
palette: <NUL> NULL
square: <lgc> FALSE
equal_axes: <lgc> FALSE
pad: <nmr> 0.04
xlim: <NUL> NULL
ylim: <NUL> NULL
xlab: <NUL> NULL
ylab: <NUL> NULL
margin_top: <NUL> NULL
margin_right: <NUL> NULL
margin_bottom: <NUL> NULL
margin_left: <NUL> NULL
Supply the data at draw time:
Or let the config name its own data, which is what makes it a standalone document. dat_path reads .csv (column names kept exactly as written) and .rds (for the charts that bind a matrix or an object rather than a table):
chart_registry() is the single list of chart types, read both when a document is loaded and when the schemas are generated, so the two cannot disagree:
[1] "scatter" "bar" "density" "histogram" "line"
[6] "pie" "boxplot" "sankey" "gantt" "network"
[11] "choropleth" "heatmap" "spectrogram" "a3"
Each entry names the class that models the type and the setup_* that builds one:
resolve(): filling in what the data determinesresolve() derives the values the data implies — axis labels from the bound column names, limits from the values — and records where each one came from:
xlab ylab
"bill_len" "flipper_len"
It is idempotent, and it never overwrites a value the author set. It also derives what it can with no data at all, since column names alone determine the labels:
Every setup_*() records an origin for each property: "user" if the author named it in the call, "default" if the constructor filled it in. resolve() marks the ones it computes as "derived":
x y fit xlab xlim pad
"user" "user" "user" "derived" "derived" "default"
This distinction is what lets a document move between interfaces with its intent intact. A margin the author set must be honored anywhere; a margin an IDE pane defaulted may be re-resolved for a large web canvas. Provenance is carried through a round trip, never recomputed — otherwise every default would harden into a choice the moment it was written.
compile(): config to render optioncompile() turns a config into the backend option object that draw() renders. It materializes the data and resolves the config before dispatching, so no chart type can skip either step:
That option is exactly what the low-level API produces by hand, so you can inspect it, edit it, and draw it:
draw(config, data = df) is compile() followed by draw(), plus any render hints the chart needs the browser to solve — a square plotting box, for instance, whose geometry depends on a container width that only the browser knows. Those hints are derived at draw time and never written into a document, because a box solved for an IDE pane is the wrong box for a large canvas.
write_chart_config() and read_chart_config() round-trip a config through JSON. By default the file is an input config: only the properties that are set, plus the origin map every setup_*() builds.
{
"type": "scatter",
"title": "Penguin bill and flipper",
"origin": {
"dat_path": "default",
"title": "user",
"x": "user",
"y": "user",
"size": "default",
"group": "user",
"fit": "user",
"se": "default",
"n_fit": "default",
"fit_alpha": "default",
"palette": "default",
"square": "default",
"equal_axes": "default",
"pad": "default",
"xlim": "default",
"ylim": "default",
"xlab": "default",
"ylab": "default",
"margin_top": "default",
"margin_right": "default",
"margin_bottom": "default",
"margin_left": "default"
},
"x": "bill_len",
"y": "flipper_len",
"group": "species",
"fit": "gam",
"se": true,
"n_fit": 200,
"fit_alpha": 0.25,
"square": false,
"equal_axes": false,
"pad": 0.040000000000000001
}
Reading is do.call(setup_*, x), so a document from any source arrives through the same seam a hand-written call goes through.
complete = TRUE writes an output config: every property, unset ones as explicit nulls, with provenance attached and this package stamped as the writer. It is the form one interface hands to another, with nothing left to infer. Resolve first, so the values the data determines are written as the derived facts they are:
{
"type": "scatter",
"dat_path": null,
"title": "Penguin bill and flipper",
"origin": {
"dat_path": "default",
"title": "user",
"x": "user",
"y": "user",
"size": "default",
"group": "user",
"fit": "user",
"se": "default",
"n_fit": "default",
"fit_alpha": "default",
"palette": "default",
"square": "default",
"equal_axes": "default",
"pad": "default",
"xlim": "derived",
"ylim": "derived",
"xlab": "derived",
"ylab": "derived",
"margin_top": "default",
"margin_right": "default",
"margin_bottom": "default",
"margin_left": "default"
},
"writer": {
"name": "rtemis.draw",
Writing a complete document requires a full origin map, which only setup_*() builds — a config from a bare constructor cannot honestly claim to be complete.
chart_config_to_list() is the same conversion without the file. It shapes each value by its declared container, so a one-element array stays an array and a map stays an object:
List of 13
$ type : chr "scatter"
$ title : chr "Penguin bill and flipper"
$ origin :List of 22
..$ dat_path : chr "default"
..$ title : chr "user"
..$ x : chr "user"
..$ y : chr "user"
..$ size : chr "default"
..$ group : chr "user"
..$ fit : chr "user"
..$ se : chr "default"
..$ n_fit : chr "default"
..$ fit_alpha : chr "default"
..$ palette : chr "default"
..$ square : chr "default"
..$ equal_axes : chr "default"
..$ pad : chr "default"
..$ xlim : chr "default"
..$ ylim : chr "default"
..$ xlab : chr "default"
..$ ylab : chr "default"
..$ margin_top : chr "default"
..$ margin_right : chr "default"
..$ margin_bottom: chr "default"
..$ margin_left : chr "default"
$ x : chr "bill_len"
$ y : chr "flipper_len"
$ group : chr "species"
$ fit : chr "gam"
$ se : logi TRUE
$ n_fit : int 200
$ fit_alpha : num 0.25
$ square : logi FALSE
$ equal_axes: logi FALSE
$ pad : num 0.04
The classes generate the JSON Schemas published at schema.rtemis.org. Each chart type publishes a schema.json (input config) and a record.json (output config), plus a dispatcher of each kind that selects a leaf on type:
[1] "$schema" "$id" "title"
[4] "description" "type" "properties"
[7] "required" "additionalProperties"
List of 4
$ type : chr [1:2] "array" "null"
$ items :List of 1
..$ type: chr "number"
$ minItems : int 2
$ description: chr "X axis limits. NULL derives them from the data."
List of 4
$ type : chr [1:2] "string" "null"
$ enum :List of 3
..$ : chr "glm"
..$ : chr "gam"
..$ : NULL
..- attr(*, "class")= chr "AsIs"
$ minLength : int 1
$ description: chr "Fit to overlay. NULL draws no fit."
Two rules the emitter enforces:
default is ever emitted. A default is what an interface chooses to fill in, not a fact about the document, and interfaces are expected to differ. Round-trip fidelity comes from writing resolved documents, not from sharing defaults.type, which carries the document’s shape. A config is partial by nature: the author sets a subset and the interface fills in the rest. The record.json variant requires every property, which is a claim about a written document rather than a constraint on what an author has to type.Each leaf is self-contained — it declares its own type constant and closes with additionalProperties: false — so it validates standalone as well as through the dispatcher.
disp <- chart_dispatcher_schema(
classes = list(ScatterConfig, BarConfig),
id = "https://schema.rtemis.org/draw/chart.schema.json",
leaf_ids = c(
"https://schema.rtemis.org/draw/scatter/schema.json",
"https://schema.rtemis.org/draw/bar/schema.json"
),
title = "Chart config",
description = "Any rtemis.draw chart config."
)
str(disp, max.level = 2)List of 8
$ $schema : chr "https://json-schema.org/draft/2020-12/schema"
$ $id : chr "https://schema.rtemis.org/draw/chart.schema.json"
$ title : chr "Chart config"
$ description: chr "Any rtemis.draw chart config."
$ type : chr "object"
$ properties :List of 1
..$ type:List of 3
$ required : 'AsIs' chr "type"
$ oneOf :List of 2
..$ :List of 1
..$ :List of 1
write_chart_schema() writes one to disk. The package’s own generation script (data-raw/generate_schemas.R) walks chart_registry() and emits the full set; just schemas writes them into a schema-repo checkout and just schemas-check verifies they build.