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:
- Read penguin data with Python or R.
- Expose it to Observable JavaScript.
- Convert the transferred data into row-oriented records.
- Filter it with a species checkbox and a bill-length slider.
- 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.
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.
#1 Best Overall
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.
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.
The Tool Desk
Outbyte PC Repair FREEClear out junk files and repair common Windows errorsFree Scan →Outbyte Driver Updater FREEFix the driver behind crashes, sound loss and screen glitchesFind Drivers →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:
Rank #2
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.
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.
---
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.
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]androws.length.
R’s NA, Python’s NaN, JavaScript’s null, and JavaScript’s undefined are not interchangeable. Normalize them when their distinction matters.
Do these 3 things before closing this tab:
1Scan for outdated or missing drivers - takes under a minute2Clear out junk files and repair common Windows errors3Fix the driver behind crashes, sound loss and screen glitchesFiles, 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.
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:
Recommended Free Tools
- 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.
Best Value
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.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:
Quick wins for a faster PC:
Fix the driver behind crashes, sound loss and screen glitchesFind Drivers →Repair Windows errors before they cause bigger problemsFix Now →Scan for outdated or missing drivers - takes under a minuteDriver Scan →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.
Crashes, No Sound, or Screen Glitches?
Random freezes, missing sound and display glitches usually trace back to one bad driver. Find and replace yours safely.Free scan · under a minutePC Slower Than It Used to Be?
A free scan shows the junk files, broken settings and background clutter dragging Windows down - then fixes them in one click.Free scan · Windows 10 & 11The 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.
Quick Recap
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.

