Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Some links on this page are affiliate links: if you buy through them we may earn a commission, at no extra cost to you.

Quarto lets you combine Markdown, Python or R, and interactive Observable JavaScript (OJS) in one document. The usual workflow is simple: prepare data during rendering with Python or R, pass the selected data to OJS with ojs_define(), and let Observable Inputs and Plot create client-side interactivity in the finished HTML file.

This guide builds that workflow with an interactive penguin explorer that can be opened without a live server.

What you will build

The finished document will:

  1. Read penguin data with Python or R.
  2. Expose it to Observable JavaScript.
  3. Convert the transferred data into row-oriented records.
  4. Filter it with a species checkbox and a bill-length slider.
  5. Render an interactive chart with Observable Plot.

The data flow is:

Python or R data frame → ojs_define() → OJS rows → Inputs → Observable Plot

Quarto renders the Python or R code first. OJS then runs in the reader’s browser, where its reactive cells respond to control changes. See Quarto’s official Observable JavaScript documentation.

Free tools Windows power users keep installed

One-click scans. No signup required.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Quarto, Observable JavaScript, and Observable are different things

Quarto

Quarto is an open-source publishing system for turning Markdown and notebook-style files into HTML, PDF, Word documents, presentations, websites, books, and dashboards. It supports engines including Jupyter, Knitr, and Observable JavaScript.

Observable JavaScript

Observable JavaScript is JavaScript executed through Observable’s reactive runtime. Unlike a conventional script, it treats cells as expressions with dependencies. When an input changes, cells that depend on it are evaluated again.

In Quarto, OJS code is placed in executable {ojs} cells. Quarto makes core Observable libraries such as Inputs and Plot available through its OJS integration.

Observable’s hosted platform

Observable’s hosted service is a separate platform for creating and sharing hosted notebooks. You do not need an Observable account to use OJS in a local Quarto document.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Install the required tools

Install Quarto

Download Quarto from the official download page, then verify the installation:

quarto check
quarto --version

Quarto releases change over time. Check the official download and release pages rather than copying an unqualified “latest version” number into a setup guide.

Python and Jupyter

Install Python, create an environment, and activate it.

python -m venv .venv

On macOS or Linux:

source .venv/bin/activate

On Windows PowerShell:

.venvScriptsActivate.ps1

Install the packages used by the example:

python -m pip install jupyter pandas

Quarto’s Python setup guide provides additional environment instructions.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

R and Knitr

Install R. RStudio or Positron is optional; Quarto can also be rendered from the command line. Install the packages needed for an R-based version:

install.packages(c("knitr", "reticulate", "palmerpenguins", "dplyr"))

You do not need to install both Python and R. Choose the language you already use. OJS does not replace the language runtime used for rendering.

Your first OJS document

Create hello-ojs.qmd:

---
title: "Hello Observable JavaScript"
format: html
---

```{ojs}
message = "Hello from Observable JavaScript"
```

`message`

```{ojs}
viewof name = Inputs.text({
  label: "Your name",
  value: "reader"
})
```

```{ojs}
`Hello, ${name}!`
```

Render it:

quarto render hello-ojs.qmd

Open the generated HTML file. When you change the text input, the greeting updates because the second OJS cell depends on name.

How OJS reactivity works

Traditional notebooks generally encourage sequential execution. Their state can depend on which cells you previously ran, and changing an earlier value may require you to rerun later cells.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

OJS is dependency-driven. The source order does not necessarily determine execution order. If a cell references price and quantity, it will be recalculated when either value changes:

```{ojs}
result = price * quantity
```

```{ojs}
viewof price = Inputs.range([0, 100], {value: 10, step: 1})
```

```{ojs}
viewof quantity = Inputs.range([0, 20], {value: 2, step: 1})
```

This is closer to a spreadsheet than to a top-to-bottom script. Prefer expressions derived from explicit inputs over mutable state and side effects such as:

let total = 0;
total += value;

That pattern can be confusing in a reactive graph.

Build an interactive penguin explorer

Place penguins.qmd and palmer-penguins.csv in the same project directory:

quarto-ojs-demo/
├── penguins.qmd
└── palmer-penguins.csv

Python version

This version uses Python for import and preparation:

What’s actually slowing this PC down?

Pick the symptom - the matching free tool is one click away.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
---
title: "Interactive Penguin Explorer"
format:
  html:
    code-fold: true
---

```{python}
import pandas as pd

penguins = pd.read_csv("palmer-penguins.csv")
ojs_define(data=penguins)
```

```{ojs}
rows = transpose(data)
```

```{ojs}
species = [...new Set(rows.map(d => d.species))]
```

```{ojs}
viewof selected_species = Inputs.checkbox(
  species,
  {
    value: species,
    label: "Species"
  }
)
```

```{ojs}
viewof minimum_bill_length = Inputs.range(
  [30, 60],
  {
    value: 35,
    step: 1,
    label: "Minimum bill length"
  }
)
```

```{ojs}
filtered = rows.filter(d =>
  selected_species.includes(d.species) &&
  d.bill_length_mm >= minimum_bill_length
)
```

```{ojs}
Plot.dot(filtered, {
  x: "bill_length_mm",
  y: "body_mass_g",
  color: "species",
  symbol: "sex",
  tip: true
}).plot({
  grid: true,
  height: 450
})
```

ojs_define(data=penguins) makes the Python object available to OJS under the name data. transpose(data) is useful because data frames may cross the language boundary in a column-oriented form, while chart code commonly expects an array of row objects.

R version

Replace the Python cell with this R cell:

```{r}
library(palmerpenguins)

data <- penguins
ojs_define(data = data)
```

The following OJS cells can remain the same. The exact serialized representation can depend on the engine and object type, so use simple data frames, inspect the transferred value, and normalize it when necessary.

Inputs and viewof

Observable Inputs includes range sliders, checkboxes, radio buttons, selects, and tables. For example:

viewof threshold = Inputs.range([0, 100])

This creates a visible control and a reactive value named threshold. Dependent cells should reference threshold, not the control’s DOM element.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Useful controls include:

Inputs.range([0, 100], {value: 50, step: 1, label: "Threshold"})
Inputs.checkbox(["Adelie", "Chinstrap", "Gentoo"], {label: "Species"})
Inputs.select(["All", "Male", "Female"], {label: "Sex"})

Every variable referenced by the filtering or chart cell should be spelled exactly as it is defined in the input cell.

Data transfer and its limits

Python or R runs during rendering; OJS interaction runs in the browser. The basic transfer is therefore one-way: rendered data is sent to the client, but changing a browser control does not automatically rerun Python or R.

Keep transferred data modest and deliberate. Complex factors, dates, timestamps, missing values, list-columns, nested objects, and large data frames can require cleanup.

  • Convert dates to ISO strings or numeric timestamps.
  • Decide how missing values should map to JavaScript.
  • Transfer only the columns needed by the visualization.
  • Aggregate or downsample large data in Python or R first.
  • Inspect the result with rows[0] and rows.length.

R’s NA, Python’s NaN, JavaScript’s null, and JavaScript’s undefined are not interchangeable. Normalize them when their distinction matters.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Files, attachments, and remote data

Read locally with Python or R

penguins = pd.read_csv("data/file.csv")

Or in R:

read.csv("data/file.csv")

Then expose the result with ojs_define().

Read a file in OJS

Quarto’s OJS integration supports attachments such as CSV, TSV, JSON, Arrow, and SQLite files:

data = FileAttachment("palmer-penguins.csv").csv({typed: true})

Ensure the file is included in the project and that the relative path is correct.

Remote sources

Browser-side fetches and CDN imports can work, but they introduce network dependence, possible CORS errors, changing data, privacy considerations, and offline failures. Local project files are the safer default for a reproducible standalone document.

Observable libraries and package imports

Quarto provides access to core Observable libraries through its bundled runtime, including Inputs and Plot. The exact versions may vary by Quarto release, so do not assume that the newest hosted Observable API is bundled.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Third-party browser-compatible packages can be loaded with require():

```{ojs}
d3 = require("d3@7")
topojson = require("topojson")
```

Quarto resolves these modules through jsDelivr. Pin versions where reproducibility matters. The library documentation also describes direct ESM imports, for example:

Plot = import("https://cdn.jsdelivr.net/npm/@observablehq/plot/+esm")

A CDN import creates an external network dependency. It can fail when the network, CDN, package, module format, or browser compatibility is unsuitable.

Render, preview, and publish

From the project directory:

quarto check
quarto render penguins.qmd
quarto preview penguins.qmd

Before publishing, test the generated HTML rather than only the editor preview:

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
  • Change every control and confirm the chart updates.
  • Test empty selections and missing values.
  • Check tooltips and mobile layout.
  • Confirm that local assets are present.
  • Test the published URL, including its network and CORS behavior.

Client-side OJS interaction generally does not require a server, making static HTML hosting a good fit. However, the browser still needs the JavaScript, data, and any CDN resources. A document that performs server-side computation or accesses protected data needs additional infrastructure.

To hide source code in one cell:

```{ojs}
#| echo: false

...
```

Or hide code document-wide:

---
execute:
  echo: false
---

See Quarto’s OJS cell options for execution and display controls.

Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Support on Ko-Fi

Troubleshooting

ojs_define is not recognized

Render with Quarto rather than opening the .qmd file directly. Confirm that the Python/Jupyter or R/Knitr engine is installed, that the language cell runs successfully, and that the ojs_define() call occurs before the OJS code that consumes the object.

The data is empty or has the wrong shape

Check the file path and earlier engine errors. In OJS, inspect:

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
data
rows.length
rows[0]

If the data is column-oriented, use:

rows = transpose(data)

The chart is blank

Inspect the column names and types, then check whether filtering returned zero rows:

filtered.length
filtered.slice(0, 3)

Verify exact names such as bill_length_mm, remove or handle missing values, and ensure the chart expression actually returns a Plot.

Inputs do not update the chart

Confirm that the control uses viewof, the chart cell references the input value, and all code is in OJS cells. A spelling mismatch or unrelated JavaScript error can break the dependency chain.

A package import fails

Check the package name and browser compatibility. Try a pinned version such as require("d3@7"). Some packages require Node-only APIs and cannot run directly in a browser-based OJS cell.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

The document works locally but fails after publishing

Look for absolute file paths, omitted project assets, blocked CDN requests, and remote data protected by CORS rules. A pure OJS document is usually easier to publish statically than a server-backed application, but external dependencies can still make it fragile.

The page is slow

Because filtering and plotting happen in the browser, avoid sending an entire database to every reader. Aggregate, select required columns, or downsample in Python/R. For large or private data, consider a server-backed design.

When should you use OJS?

Need Good choice Why
Static HTML with browser-side controls OJS Reactive interaction without a live application server.
Interactive charts with little JavaScript Python/R widgets Jupyter Widgets and R htmlwidgets can keep most work in the author’s preferred language.
Server-side computation, private data, authentication, or persistent state Shiny More flexible, but requires server deployment.
Conventional application lifecycle or reusable JavaScript package Plain JavaScript or a framework Provides more direct lifecycle and architecture control.
Collaborative hosted notebooks Observable hosted platform Designed for hosted notebook collaboration; it is not required for local Quarto OJS.

OJS is strongest when the dataset is small enough to send to the browser and the interaction is a document feature rather than a full application.

Reusable project template

---
title: "Interactive Data Explorer"
format: html
---

```{python}
import pandas as pd

raw = pd.read_csv("data.csv")
# Keep only the fields needed in the browser.
data = raw[["category", "x", "y"]].dropna()
ojs_define(data=data)
```

```{ojs}
rows = transpose(data)
categories = [...new Set(rows.map(d => d.category))]
```

```{ojs}
viewof selected = Inputs.checkbox(categories, {
  value: categories,
  label: "Category"
})
```

```{ojs}
filtered = rows.filter(d => selected.includes(d.category))
```

```{ojs}
Plot.dot(filtered, {
  x: "x",
  y: "y",
  color: "category",
  tip: true
}).plot({grid: true})
```

For an R workflow, replace the Python cell with a Knitr cell that creates a plain data frame and calls ojs_define(data = data). Keep the data boundary explicit, inspect the transferred object, and let OJS handle browser-side controls and rendering.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.