Skip to content

Rich Output

Interactive cells can return multiple typed output artifacts. Plain print and println! still work, and richer artifacts render with shared UI components.

Oxiquill renders these output kinds:

  • Text from stdout, stderr, or display output.
  • JSON values with structured formatting.
  • Tables with sortable columns.
  • Charts rendered with ECharts.
  • Images from SVG, PNG, or JPEG data.
  • Sandboxed HTML in an iframe.

Every artifact may have optional string id, title, and caption fields. Values received from workers are untrusted: Oxiquill validates discriminators, fields, plain-record structure, array shapes, finite numbers, row widths, MIME/base64 data, and nested values before a renderer receives them. One rejected or failing artifact shows a local diagnostic without removing valid siblings.

KindRequired fieldsOptional behavior
textstream: "stdout" | "stderr" | "display", content: stringtruncated indicates bounded content.
jsonvalueCycle-aware formatting represents BigInt explicitly; truncated indicates bounded serialized data.
tablecolumns, rectangular rowsrowCount records the original size; truncated marks a preview.
chartValid spec described belowOversize/invalid charts become a local artifact error.
imagemime: image/png | image/jpeg | image/svg+xml, valid dataalt supplies the image alternative.
htmlhtml: string, sandboxed: trueRendered with an empty sandbox, no referrer, and the restrictive CSP described below.

Table columns contain key, label, and optional type: string, number, integer, boolean, date, datetime, null, or unknown. Every row must have exactly the declared column count.

All chart specs accept optional title, xLabel, yLabel, xType, yType, legend, tooltip, and dataZoom. Axis types are value, category, time, and log. Unspecified axes default to value, except the category axes intrinsic to bar/histogram charts. A heatmap always has category axes on both dimensions: its xType and yType may be omitted or set to category, while value, time, and log are rejected.

  • value coordinates are finite numbers, and log coordinates are finite numbers greater than zero.

  • time coordinates are finite epoch milliseconds or ISO-8601 strings in YYYY-MM-DDTHH:mm:ss[.sss]Z or YYYY-MM-DDTHH:mm:ss[.sss]±HH:mm form. Calendar fields and the explicit time zone must be valid.

  • category coordinates are strings or finite numbers. For a heatmap axis with an explicit xCategories or yCategories array, each coordinate must be an exact listed string or a safe, in-range zero-based integer index. An index resolves to the category name at that position; it is never converted into a new numeric-looking category name.

  • For a heatmap axis without an explicit categories array, finite numbers and strings are normalized with String(coordinate). Categories are inferred in first-appearance order, so values with the same normalized label, such as 1 and "1", share one category. X and Y are normalized independently when only one axis declares categories.

  • line, scatter, and area: series[], each with optional name and [x, y] points.

  • bar: string categories[] plus series of numeric-or-null values[] with matching length.

  • histogram: bins[] as finite [lower, upper, count] tuples, where lower < upper and count is non-negative.

  • heatmap: optional string xCategories/yCategories arrays and finite [x, y, value] cells on two category axes.

Empty charts and equal point domains are valid. Heatmaps report X- and Y-category counts plus the heat-value range; their color scale uses the actual heat values, with a 0–1 fallback only for an empty dataset. Charts expose a textual title/caption and bounded accessible summary or equivalent table. The implementation canvas is hidden from accessibility APIs when that equivalent is present.

Charts use contrast-aware colors for the current Starlight light/dark theme, recreate their ECharts instance when html[data-theme] changes, and expose a retry action after a transient renderer load failure.

The last successful output stays visible while a cell reruns, including the 150ms reactive debounce. A small updating indicator does not move the layout. Failed reruns keep that output alongside an error and a Retry cell action; cancellation and invalid inputs also retain it. Success replaces the result and clears the error. Use explicit artifact IDs when an output sequence changes dynamically: IDs retain identity across insertions, duplicate IDs receive occurrence numbers, and artifacts without IDs are matched by kind and occurrence.

Compatible charts update the existing canvas and ECharts instance, preserving the reader’s zoom window. Initial and data-update transitions use 180ms with cubicOut easing. Structural changes replace chart options on the same instance; Starlight light/dark theme changes recreate it. A chart update failure keeps the last successful visualization and summary with a local retry action. Reduced-motion preferences disable chart and updating-indicator animation, even when an author requests animation.

Chart specs accept an optional nested style object:

interface ChartPalette {
light?: readonly string[];
dark?: readonly string[];
}
interface BaseChartStyle {
palette?: ChartPalette;
showGrid?: boolean;
animation?: boolean;
animationDurationMs?: number;
}
interface LineChartStyle extends BaseChartStyle {
lineWidth?: number;
}
interface ScatterChartStyle extends BaseChartStyle {
symbolSize?: number;
}

Line and area charts use LineChartStyle, scatter charts use ScatterChartStyle, and bar, histogram, and heatmap charts use BaseChartStyle.

FieldAllowed values and default
paletteAt least one of light or dark; each supplied theme has 1–12 unique #RGB or #RRGGBB colors. Heatmaps require at least two. Colors are compared after expanding shorthand and ignoring case.
showGridBoolean; default true.
animationBoolean; default true, subordinate to reduced motion.
animationDurationMsInteger from 0 to 2,000; default 180 for both initial and update animation.
lineWidthFinite number from 1 to 8; default 2.25. Line and area only.
symbolSizeFinite number from 2 to 32; default 7. Scatter only.

Palettes color series, or the visual-map gradient for heatmaps. A missing theme palette uses the Oxiquill default for that theme. Chart chrome and typography follow Starlight. Unknown nested fields, fields for another chart kind, raw ECharts options, callbacks, formatters, and HTML are rejected. Style contributes to the normal artifact byte budget; it does not bypass output limits.

Every existing Rust chart macro signature remains valid. These overloads add a final JSON-serializable style argument, such as &serde_json::json!({ "lineWidth": 3 }):

emit_line_chart!(series, style)
emit_line_chart!(series, x_label, y_label, style)
emit_scatter_chart!(series, style)
emit_scatter_chart!(series, x_label, y_label, style)
emit_bar_chart!(categories, values, style)
emit_histogram!(bins, style)
emit_heatmap!(data, style)
emit_line_plot!(points, x_label, y_label, style)

The macro serializes the argument into spec.style; normal chart validation enforces the same contract.

Producers enforce limits inside each language worker before postMessage; the main thread independently validates the bounded response again. Limits are UTF-8 byte limits and are cumulative for each run where noted:

ResourceLimitOver-limit result
Artifacts100 per runAdditional artifacts are rejected locally.
stdout or stderr1 MiB per streamCaptured text is retained only to the limit and marked truncated.
Text, serialized JSON, HTML1 MiB per artifactText/JSON may use truncated: true; HTML is rejected.
Table10,000 rows and 100 columnsA bounded table may use truncated: true and rowCount.
Chart100,000 total points/bins/cellsArtifact-local error.
Image10 MiB decoded bytesArtifact-local error.
All validated output16 MiB per runArtifacts exceeding the remaining budget are rejected locally.
Complete worker response16 MiBLater artifacts are omitted before worker-to-page transfer.
Worker error16 KiBThe UTF-8 message is truncated.
Artifact diagnostic8 KiB each; 64 KiB per runDiagnostics are truncated within both limits.

truncated: true is valid only for text, JSON, and table artifacts. Chart, image, and HTML artifacts are never silently truncated. Invalid/cyclic/over-deep JSON, ragged tables, non-finite chart values, invalid base64/MIME pairs, or unsafe structural records cannot crash the cell component. Legacy stdout, stderr, value, and plots aliases are regenerated from validated, bounded outputs; raw aliases cannot retain a second unbounded payload.

The default table action copies visible rows as spreadsheet-safe CSV. String headers or cells whose first meaningful character is =, +, -, @, tab, or carriage return receive a leading apostrophe before RFC-style comma/quote/line-break escaping. This deterministic convention prevents common spreadsheet applications from evaluating string data as a formula. Numeric values remain numeric, so a genuine number such as -42 is copied as -42.

Python cells include display helpers:

  • display(value) chooses JSON, table, image, HTML, or text output from the value.
  • display_json(value) emits JSON.
  • display_html(html) emits sandboxed HTML.
  • display_table(rows_or_dataframe) emits a table.
  • display_image(data, mime) emits SVG, PNG, or JPEG image data.

The runtime also understands common rich representations such as pandas DataFrames and Series, matplotlib figures, _repr_mimebundle_, _repr_json_, _repr_html_, _repr_svg_, _repr_png_, and _repr_jpeg_.

Python + Pyodide

Python table, matplotlib, HTML, and JSON outputs

Cell actions
import pandas as pd
import matplotlib.pyplot as plt

scores = pd.DataFrame({"label": ["alpha", "beta", "gamma"], "score": [3, 5, 4]})

display(scores, title="Pandas table")
display_json({"status": "ok", "rows": len(scores)}, title="Summary")
display_html("<p><strong>Sandboxed HTML</strong> emitted from Python.</p>", title="HTML")

plt.bar(scores["label"], scores["score"])
_ = plt.title("Scores")
_ = plt.xlabel("label")
_ = plt.ylabel("score")

Run the cell to show its output.

Rust cells use explicit emit_* macros. Supported macros include:

  • emit_text!
  • emit_json!
  • emit_html!
  • emit_svg! and emit_image_svg!
  • emit_png_base64! and emit_image_png!
  • emit_table!, emit_table_with_columns!, and emit_records_table!
  • emit_line_chart!, emit_scatter_chart!, emit_bar_chart!, emit_histogram!, and emit_heatmap!
  • emit_line_plot! for compatibility with the original line-plot helper

Rust + Wasm

Rust table, chart, JSON, SVG, and HTML outputs

Cell actions
let rows = vec![
    serde_json::json!({"label": "alpha", "score": 3}),
    serde_json::json!({"label": "beta", "score": 5}),
    serde_json::json!({"label": "gamma", "score": 4}),
];
let columns = vec![
    serde_json::json!({"key": "label", "label": "Label", "type": "string"}),
    serde_json::json!({"key": "score", "label": "Score", "type": "integer"}),
];
let heatmap = [[0, 0, 1], [1, 0, 3], [0, 1, 2], [1, 1, 4]];

emit_json!(&serde_json::json!({"status": "ok", "rows": rows.len()}));
emit_records_table!(&rows);
emit_table_with_columns!(&columns, &rows);
emit_bar_chart!(&["alpha", "beta", "gamma"], &[3, 5, 4], &serde_json::json!({
    "palette": { "light": ["#7c3aed"], "dark": ["#c4b5fd"] },
    "animationDurationMs": 180
}));
emit_heatmap!(&heatmap);
emit_svg!(
    r##"<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 160 48"><rect width="160" height="48" fill="#ecfeff"/><circle cx="32" cy="24" r="14" fill="#0f766e"/><rect x="64" y="12" width="72" height="24" rx="4" fill="#2563eb"/></svg>"##,
    "Simple generated SVG"
);
emit_html!(r#"<p><strong>Sandboxed HTML</strong> emitted from Rust.</p>"#);

Run the cell to show its output.

Use stdout for short explanatory text. Use JSON when readers need the exact structured value. Use tables for comparable records, charts for trends or distributions, images for generated visuals, and sandboxed HTML only when the output needs markup that cannot be represented by the other artifact types.

Copy, sort, pagination, validation, running, completion, and failure states are keyboard-operable and announced with localized English/Japanese labels. Sandboxed HTML intentionally has no script or same-origin permission, sends no referrer, and blocks external subresources by default; see Support and Security for the exact policy and trust boundary.

Table interaction state is scoped to one execution result. A new result resets the page, sort, and copy status. Rerenders of the same result preserve a compatible sort and page, but row shrinkage stores the clamped page and a removed or replaced sorted column clears the sort.