---
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).
## Define data
```{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"
}
]
```
## Output widgets
### Table
```{ojs}
viewof table_a = Inputs.table(data_hdi)
```
### Plot
```{ojs}
//| label: barplot
Plot.plot({
marginLeft: 100,
y: {
},
marks: [
Plot.barX(data_hdi, {
x: "hdi",
y: "name",
title: (d) =>
`Country: ${d.name}`
})
],
grid: true
})
```
## Input widgets
### 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}.
### 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
```{ojs}
viewof dflavors = Inputs.select(["salty", "sweet", "bitter", "sour", "umami"], {label: "Flavors", multiple: true})
```
Selected items: ${dflavors}.
### 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"})
```
## 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')
```
### Image 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 an image as PNG.
```{ojs}
downloadHtmlAsImage(barplot)
```
Adjust the scale parameter.
```{ojs}
downloadHtmlAsImage(barplot, {
label: "Download 4X PNG",
filename: "scaled_4x",
scale: 4
})
```
Download image as SVG.
```{ojs}
import {serialize} from "@mbostock/saving-svg"
import {rasterize} from "@mbostock/saving-svg"
```
```{ojs}
DOM.download(() => serialize(barplot), undefined, "Save as SVG")
```
### Zip download
```{ojs}
JSZip = require('https://unpkg.com/jszip@3.1.5/dist/jszip.min.js')
zip = {
const zip = new JSZip()
const folder = zip.folder('website')
folder.file('data_hdi.txt', data_hdi)
return zip.generateAsync({type: 'blob'})
}
```
```{ojs}
DOM.download(zip, 'website.zip')
```