Render df and spec as a complete Vega-Lite v5 specification JSON
string: $schema (pinned to the Vega-Lite v5 schema URL) + optional
title + mark (from spec.kind) + encoding (x / y and, when
set, color, each {field, type}) + data.values (the frame inlined
as JSON records, sharing format_json's cell mapping — null and
non-finite-float cells become JSON null). A frame with the encoded
columns but zero rows yields "data":{"values":[]}.
Each encoding channel's field type follows its column's dtype — the
single statement of the dtype→type rule: a numeric column (Int /
Float) is "quantitative", a Date column is "temporal" (its ISO
cells — data.values' date units, via the JSON records' ISO mapping —
sort chronologically), and every other dtype is "nominal" — unless
the channel carries an explicit color_type override.
The spec's x / y / color columns are resolved left-to-right; the
first name absent from df raises ColumnNotFound(name) — this is the
one reason the function is not total (it accepts column names, and a
name can be wrong). The emitted text is always valid JSON (it is built
through @json and stringified), so it round-trips through any
standards-compliant JSON / Vega-Lite reader.