---
title: ObservableJS in Quarto
subtitle: Demonstration of widgets, upload and download
author: "Roy Francis"
date: last-modified
date-format: "DD-MMM-YYYY"
format:
html:
fig-align: left
title-block-banner: true
toc: true
number-sections: true
code-tools: true
code-fold: true
---
See the [ official OJS documentation ](https://observablehq.com/documentation/inputs/overview) .
:::{.callout-note}
Note that OJS does not work when opening a static local HTML file in the browser. It requires a live server. For local viewing, use `quarto preview` , `python -m http.server` etc. It will just work when hosted on a server/hosting service such as Github pages etc.
:::
## Define data
Data is defined as JSON arrays.
```{ojs}
data_hdi = [
{
name: "Switzerland",
continent: "Europe",
hdi: "0.962"
},
{
name: "Norway",
continent: "Europe",
hdi: "0.961"
},
{
name: "Iceland",
continent: "Europe",
hdi: "0.959"
},
{
name: "Hong Kong",
continent: "Asia",
hdi: "0.952"
},
{
name: "Australia",
continent: "Oceania",
hdi: "0.951"
},
{
name: "Denmark",
continent: "Europe",
hdi: "0.948"
},
{
name: "Sweden",
continent: "Europe",
hdi: "0.947"
},
{
name: "Ireland",
continent: "Europe",
hdi: "0.945"
},
{
name: "Germany",
continent: "Europe",
hdi: "0.942"
},
{
name: "Netherlands",
continent: "Europe",
hdi: "0.941"
},
{
name: "Finland",
continent: "Europe",
hdi: "0.940"
},
{
name: "Singapore",
continent: "Asia",
hdi: "0.939"
},
{
name: "Belgium",
continent: "Europe",
hdi: "0.937"
},
{
name: "New Zealand",
continent: "Oceania",
hdi: "0.937"
},
{
name: "Canada",
continent: "North America",
hdi: "0.936"
}
]
```
## Widgets
### Table
```{ojs}
viewof table_a = Inputs.table(data_hdi)
```
### Text
```{ojs}
viewof name = Inputs.text({label: "Name", placeholder: "What’s your name?"})
```
Your input is ${name}
### Text area input
```{ojs}
viewof bio = Inputs.textarea({label: "Biography", placeholder: "What’s your story?"})
```
Your input is ${bio}
### Range (Numeric)
```{ojs}
viewof hdi = Inputs.range([0.90, 1.00], {value: 0.98, step: 0.01, label: "HDI"})
```
Below are countries with HDI less than ${hdi.toFixed(3)}
```{ojs}
data_numeric = data_hdi.filter(d => d.hdi < hdi)
viewof table_numeric = Inputs.table(data_numeric)
```
### Button
```{ojs}
viewof clicks = Inputs.button("Click me")
```
Number of clicks: ${clicks}
### Toggle button
```{ojs}
viewof mute = Inputs.toggle({label: "Mute"})
```
Current state: ${mute}
### Radio button
```{ojs}
viewof cont = Inputs.radio(["Asia", "Europe", "North America", "Oceania"], {label: "Select continent:", value: "Europe"})
```
The table below is filtered to keep ${cont}.
```{ojs}
data_radio = data_hdi.filter(d => d.continent == cont)
viewof table_radio = Inputs.table(data_radio)
```
### Checkbox
```{ojs}
viewof flavors = Inputs.checkbox(["salty", "sweet", "bitter", "sour", "umami"], {label: "Flavors"})
```
Selected items: ${flavors}
### Dropdown selection
#### Single selection
```{ojs}
viewof dflavors1 = Inputs.select(["salty", "sweet", "bitter", "sour", "umami"], {label: "Flavors", multiple: false})
```
Selected items: ${dflavors1}
#### Multiple selection
```{ojs}
viewof dflavors2 = Inputs.select(["salty", "sweet", "bitter", "sour", "umami"], {label: "Flavors", multiple: true})
```
Selected items: ${dflavors2}
### Date
```{ojs}
viewof birthday = Inputs.date({label: "Select date"})
```
Selected date: ${birthday}
### Color
```{ojs}
viewof color = Inputs.color({label: "Favorite color", value: "#4682b4"})
```
Selected color: ${color}
### File upload
```{ojs}
viewof file = Inputs.file({label: "Upload a CSV file:", accept: ".csv", required: true})
data3 = file.csv({typed: true})
viewof raw_table = Inputs.table(data3)
```
### Search
```{ojs}
//viewof search = Inputs.search(data_hdi, { placeholder: "Search"})
viewof search = Inputs.search(data_hdi, {
placeholder: "Search",
value: "",
oninput: function(event) {
const searchTerm = event.target.value.toLowerCase();
const filteredData = data_hdi.filter(item =>
item.name.toLowerCase().includes(searchTerm)
);
return filteredData;
}
});
viewof table_b = Inputs.table(search)
```
### Forms
Forms can be used to combine multiple inputs as a set.
```{ojs}
viewof rgb = Inputs.form([
Inputs.range([0, 255], {step: 1, label: "r"}),
Inputs.range([0, 255], {step: 1, label: "g"}),
Inputs.range([0, 255], {step: 1, label: "b"})
]);
rgb
```
```{ojs}
function rgbString (array) {
return 'rgb(' + array.join(', ') + ')';
}
displayColor = function (rgb) {
return html`
<section style="width: 400px; display: grid; grid-gap: 10px;">
<div class="color" style="grid-column: 1;">
<div class="swatch" style="background: ${rgbString(rgb)}; padding: 10px; border-radius:5px;">
<span>${rgbString(rgb)}</span>
</div>
</div>
</section>`
}
displayColor(rgb);
```
OJS input widgets are documented [ here ](https://observablehq.com/framework/inputs) .
## Downloads
### Text download
```{ojs}
button = (data, filename = 'data.csv') => {
if (!data) throw new Error('Array of data required as first argument');
let downloadData;
if (filename.includes('.csv')) {
downloadData = new Blob([d3.csvFormat(data)], { type: "text/csv" });
} else {
downloadData = new Blob([JSON.stringify(data, null, 2)], {
type: "application/json"
});
}
const size = (downloadData.size / 1024).toFixed(0);
const button = DOM.download(
downloadData,
filename,
`Download ${filename} (~${size} KB)`
);
return button;
}
```
```{ojs}
button(data_hdi, 'hdi.csv')
```
### Zip download
Download table as a zipped file.
```{ojs}
JSZip = require('https://unpkg.com/jszip@3.1.5/dist/jszip.min.js')
zip = {
const zip = new JSZip()
const folder = zip.folder('data')
folder.file('data_hdi.txt', data_hdi)
return zip.generateAsync({type: 'blob'})
}
```
```{ojs}
DOM.download(zip, 'data.zip')
```
### Plot
```{ojs}
//| label: barplot
barplot = Plot.plot({
marginLeft: 100,
y: {
},
marks: [
Plot.barX(data_hdi, {
x: "hdi",
y: "name"
})
],
grid: true
})
barplot
```
Now with interactive tooltips.
```{ojs}
//| label: barplot-1
barplot1 = Plot.plot({
marginLeft: 100,
y: {
},
marks: [
Plot.barX(data_hdi, {
x: "hdi",
y: "name",
tip: true,
channels: {country: "name"}
})
],
grid: true
})
barplot1
```
### Image download
This is based on [ mbostock/saving-svg ](https://observablehq.com/@mbostock/saving-svg) .
Download image as SVG.
```{ojs}
DOM.download(() => serialize(barplot), undefined, "Save as SVG")
```
Download image as PNG.
```{ojs}
DOM.download(() => rasterize(barplot), undefined, "Save as PNG")
```
```{ojs}
serialize = {
const xmlns = "http://www.w3.org/2000/xmlns/";
const xlinkns = "http://www.w3.org/1999/xlink";
const svgns = "http://www.w3.org/2000/svg";
return function serialize(svg) {
svg = svg.cloneNode(true);
const fragment = window.location.href + "#";
const walker = document.createTreeWalker(svg, NodeFilter.SHOW_ELEMENT);
while (walker.nextNode()) {
for (const attr of walker.currentNode.attributes) {
if (attr.value.includes(fragment)) {
attr.value = attr.value.replace(fragment, "#");
}
}
}
svg.setAttributeNS(xmlns, "xmlns", svgns);
svg.setAttributeNS(xmlns, "xmlns:xlink", xlinkns);
const serializer = new window.XMLSerializer;
const string = serializer.serializeToString(svg);
return new Blob([string], {type: "image/svg+xml"});
};
}
function rasterize(svg) {
let resolve, reject;
const promise = new Promise((y, n) => (resolve = y, reject = n));
const image = new Image;
image.onerror = reject;
image.onload = () => {
const rect = svg.getBoundingClientRect();
const context = DOM.context2d(rect.width, rect.height);
context.drawImage(image, 0, 0, rect.width, rect.height);
context.canvas.toBlob(resolve);
};
image.src = URL.createObjectURL(serialize(svg));
return promise;
}
```
### HTML download
```{ojs}
FileSaver = (await import("https://cdn.skypack.dev/file-saver@2.0.5?min"))
.default
html2canvas = (await import("https://esm.sh/html2canvas@1.4.1")).default
async function canvas2blob(canvas) {
let resolve, reject;
const promise = new Promise((y, n) => ((resolve = y), (reject = n)));
canvas.toBlob((blob) => {
if (blob == null) {
return reject();
}
resolve(blob);
});
return promise;
}
function generateDownloader(el, options) {
return async function () {
let resolve, reject;
const { filename = "untitled", ...html2canvasOptions } = options;
const canvas = await html2canvas(el, html2canvasOptions);
const blob = await canvas2blob(canvas);
FileSaver(blob, filename);
};
}
function downloadHtmlAsImage(el, options) {
const { label, ...restOptions } = Object.assign(
{ label: "Download as PNG", scale: window.devicePixelRatio },
options
);
const ui = Inputs.button(label, {
value: null,
reduce: generateDownloader(el, restOptions)
});
return ui;
}
```
Download HTML element as PNG.
```{ojs}
downloadHtmlAsImage(barplot1)
```
Adjust the scale parameter.
```{ojs}
downloadHtmlAsImage(barplot1, {
label: "Download 4X PNG",
filename: "scaled_4x",
scale: 4
})
```