ObservableJS in Quarto

Demonstration of widgets, upload and download

Author

Roy Francis

Published

06-Sep-2024

See the official OJS documentation.

1 Define data

Code
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"
  }
]

2 Output widgets

2.1 Table

Code
viewof table_a = Inputs.table(data_hdi)

2.2 Plot

Code
Plot.plot({
  marginLeft: 100,
  y: {

  },
  marks: [
    Plot.barX(data_hdi, {
      x: "hdi",
      y: "name",
      title: (d) =>
        `Country: ${d.name}`
    })
  ],
  grid: true
})

3 Input widgets

3.1 Text

Code
viewof name = Inputs.text({label: "Name", placeholder: "What’s your name?"})

Your input is .

3.2 Text area input

Code
viewof bio = Inputs.textarea({label: "Biography", placeholder: "What’s your story?"})

Your input is .

3.3 Numeric

Code
viewof hdi = Inputs.range([0.90, 1.00], {value: 0.98, step: 0.01, label: "HDI"})

Below are countries with HDI less than .

Code
data_numeric = data_hdi.filter(d => d.hdi < hdi)
viewof table_numeric = Inputs.table(data_numeric)

3.4 Button

Code
viewof clicks = Inputs.button("Click me")

Number of clicks: .

3.5 Toggle button

Code
viewof mute = Inputs.toggle({label: "Mute"})

Current state: .

3.6 Radio button

Code
viewof cont = Inputs.radio(["Asia", "Europe", "North America", "Oceania"], {label: "Select continent:", value: "Europe"})

The table below is filtered to keep .

Code
data_radio = data_hdi.filter(d => d.continent == cont)
viewof table_radio = Inputs.table(data_radio)

3.7 Checkbox

Code
viewof flavors = Inputs.checkbox(["salty", "sweet", "bitter", "sour", "umami"], {label: "Flavors"})

Selected items: .

3.9 Date

Code
viewof birthday = Inputs.date({label: "Select date"})

Selected date: .

3.10 Color

Code
viewof color = Inputs.color({label: "Favorite color", value: "#4682b4"})

Selected color: .

3.11 File upload

Code
viewof file = Inputs.file({label: "Upload a CSV file:", accept: ".csv", required: true})
data3 = file.csv({typed: true})
viewof raw_table = Inputs.table(data3)

4 Downloads

4.1 Text download

Code
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;
}
Code
button(data_hdi, 'hdi.csv')

4.2 Image download

Code
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.

Code
downloadHtmlAsImage(barplot)

Adjust the scale parameter.

Code
downloadHtmlAsImage(barplot, {
    label: "Download 4X PNG",
    filename: "scaled_4x",
    scale: 4
})

Download image as SVG.

Code
import {serialize} from "@mbostock/saving-svg"
import {rasterize} from "@mbostock/saving-svg"
Code
DOM.download(() => serialize(barplot), undefined, "Save as SVG")

4.3 Zip download

Code
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'})
}
Code
DOM.download(zip, 'website.zip')