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:
<rtemis.draw::ScatterConfig>
@ type : chr "scatter"
@ dat_path : NULL
@ title : chr "Penguin bill and flipper"
@ origin : Named chr [1:33] "default" "user" "default" "default" "user" "user" ...
.. - attr(*, "names")= chr [1:33] "dat_path" "title" "legend_position" "legend_placement" ...
@ writer : NULL
@ legend_position : chr "top"
@ legend_placement: chr "outside"
@ x : chr "bill_len"
@ y : chr "flipper_len"
@ size : NULL
@ group : chr "species"
@ hover : NULL
@ fit_name : NULL
@ rug : logi FALSE
@ fit : chr "gam"
@ fit_params : NULL
@ se : logi TRUE
@ se_times : num 1.96
@ rsq : logi FALSE
@ diagonal : logi FALSE
@ diagonal_color : NULL
@ n_fit : int 200
@ fit_alpha : num 0.25
@ point_alpha : NULL
@ palette : NULL
@ square : logi FALSE
@ equal_axes : logi FALSE
@ pad : num 0.04
@ xlim : NULL
@ ylim : NULL
@ xlab : NULL
@ ylab : NULL
@ margin_top : NULL
@ margin_right : NULL
@ margin_bottom : NULL
@ margin_left : 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] "scatter3d" "timeseries" "survival" "calibration" "roc"
[6] "scatter" "confusion" "significance" "bar" "density"
[11] "histogram" "line" "pie" "boxplot" "sankey"
[16] "gantt" "network" "choropleth" "heatmap" "spectrogram"
[21] "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",
"legend_position": "default",
"legend_placement": "default",
"x": "user",
"y": "user",
"size": "default",
"group": "user",
"hover": "default",
"fit_name": "default",
"rug": "default",
"fit": "user",
"fit_params": "default",
"se": "default",
"se_times": "default",
"rsq": "default",
"diagonal": "default",
"diagonal_color": "default",
"n_fit": "default",
"fit_alpha": "default",
"point_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"
},
"legend_position": "top",
"legend_placement": "outside",
"x": "bill_len",
"y": "flipper_len",
"group": "species",
"rug": false,
"fit": "gam",
"se": true,
"se_times": 1.96,
"rsq": false,
"diagonal": false,
"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",
"legend_position": "default",
"legend_placement": "default",
"x": "user",
"y": "user",
"size": "default",
"group": "user",
"hover": "default",
"fit_name": "default",
"rug": "default",
"fit": "user",
"fit_params": "default",
"se": "default",
"se_times": "default",
"rsq": "default",
"diagonal": "default",
"diagonal_color": "default",
"n_fit": "default",
"fit_alpha": "default",
"point_alpha": "derived",
"palette": "default",
"square": "default",
"equal_axes": "default",
"pad": "default",
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 19
$ type : chr "scatter"
$ title : chr "Penguin bill and flipper"
$ origin :List of 33
..$ dat_path : chr "default"
..$ title : chr "user"
..$ legend_position : chr "default"
..$ legend_placement: chr "default"
..$ x : chr "user"
..$ y : chr "user"
..$ size : chr "default"
..$ group : chr "user"
..$ hover : chr "default"
..$ fit_name : chr "default"
..$ rug : chr "default"
..$ fit : chr "user"
..$ fit_params : chr "default"
..$ se : chr "default"
..$ se_times : chr "default"
..$ rsq : chr "default"
..$ diagonal : chr "default"
..$ diagonal_color : chr "default"
..$ n_fit : chr "default"
..$ fit_alpha : chr "default"
..$ point_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"
$ legend_position : chr "top"
$ legend_placement: chr "outside"
$ x : chr "bill_len"
$ y : chr "flipper_len"
$ group : chr "species"
$ rug : logi FALSE
$ fit : chr "gam"
$ se : logi TRUE
$ se_times : num 1.96
$ rsq : logi FALSE
$ diagonal : logi FALSE
$ 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" "allOf"
List of 4
$ type : chr [1:2] "array" "null"
$ items :List of 1
..$ type: chr "number"
$ minItems : int 2
$ description: chr "X axis limits. Unset derives them from the data."
List of 3
$ type : chr [1:2] "string" "null"
$ minLength : int 1
$ description: chr "Fit to overlay: glm, gam, or an rtemis supervised learning algorithm name. Unset 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.
The generated JSON Schemas describe property types, allowed values, array shapes, and structural relationships between settings. For example, axis limits contain exactly two numbers; confusion colors use six-digit hex values; confidence bounds are paired; and grouped lines cannot also bind shading blocks. R validation and the generated schema use the same structural rule declarations.
An input configuration may omit settings for an interface to resolve. A complete record includes every setting and its provenance. An omitted confidence-bound binding can therefore await resolution in an input document, while an explicit pair containing one bound and one null is inconsistent. Schemas do not inject an interface’s defaults into another interface’s document.
Passing JSON Schema validation does not establish that a dataset can be drawn. Construction and compilation additionally check ordered limits and bin edges, column existence and alignment, probabilities, matrix dimensions, and statistical constraints such as hypothesis-family size. Those checks require relationships between numerical values or access to data outside the configuration. Treat schema validation as the document check, followed by the compiler’s semantic validation of the resolved configuration and its data.