import { aq, op } from "@uwdata/arquero"
d3 = require('d3')
books = await d3.json(
  // This is my live API so it runs in your browser.
  // Use your local API URL on your computer.
  "https://api.andrewheiss.com/books_simple?year=" + year_to_show
)
// Calculate the percent of the goal
goal_books = 50
total_books = books.count[0] == 0 ? 0 : books.count[0]
pct_goal = total_books / goal_books
pct_goal_truncated = pct_goal >= 1 ? 1 : 0
// Calculate the average rating from the full data
books_full = aq.from(books.full_data)  // Make an Arquero data frame
avg_rating = books_full
  .rollup({
    rating: d => op.mean(d.rating)
  })
text_avg_rating = avg_rating.get('rating', 0).toFixed(2)
// Calculate the percent of the year
// This is soooo janky and sad and cobbled together with zombie code from 
// GitHub Copilot, but it works, so whatever
year_info = {
  const empty_date = new Date();
  const start_of_year = new Date(empty_date.getFullYear(), 0, 1);
  const end_of_year = new Date(empty_date.getFullYear() + 1, 0, 1);
  const year_progress = ((empty_date - start_of_year) / (end_of_year - start_of_year));
  const isLeapYear = (empty_date.getFullYear() % 4 == 0) && (empty_date.getFullYear() % 100 != 0) || (empty_date.getFullYear() % 400 == 0);
  const daysInYear = isLeapYear ? 366 : 365;
  const diff = empty_date - start_of_year;
  const oneDay = 1000 * 60 * 60 * 24;
  const day = Math.floor(diff / oneDay) + 1;
  
  const text = "Day " + day + " of " + daysInYear;
  return {
    pct_year: year_progress, 
    days_in_year: daysInYear, 
    yday: day,
    text: text
  }
}
// Make a little Arquero dataframe of progress details for plotting
progress_data = aq.from([
  {type: "Year complete", name: "Completed", value: year_info.pct_year, 
   label_right: `${(year_info.pct_year * 100).toFixed(2)}%`, 
   label_left: year_info.text},
  {type: "Year complete", name: "Remaining", value: 1 - year_info.pct_year},
  
  {type: "Books", name: "Completed", value: pct_goal_truncated, 
   label_right: `${(pct_goal * 100).toFixed(2)}%`, 
   label_left: op.round(total_books) + " of " + goal_books + " books"},
  {type: "Books", name: "Remaining", value: 1 - pct_goal_truncated}
])Plot.plot({
  color: {
    range: ["#6621B9", "#868e96"]
  },
  x: {axis: null},
  y: {axis: null},
  marks: [
    Plot.barX(progress_data, {
      x: "value", 
      y: "type", 
      fill: "name"
    }),
    Plot.text(progress_data.filter(d => d.name == "Completed"), {
      x: 0,
      y: "type",
      text: "label_left",
      fill: "white",
      frameAnchor: "middle",
      textAnchor: "start",
      dx: 5,
      fontWeight: "bold",
      fontSize: 15,
      fontFamily: "Inter"
    }),
    Plot.text(progress_data.filter(d => d.name == "Completed"), {
      x: 1,
      y: "type",
      text: "label_right",
      fill: "white",
      frameAnchor: "middle",
      textAnchor: "end",
      dx: -5,
      fontWeight: "bold",
      fontSize: 15,
      fontFamily: "Inter"
    })
  ]
})
Total books
Average rating
Plot.plot({
  y: {
    label: "Books read",
    grid: false,
    percent: false
  },
  x: {
    label: "Month",
    domain: books.monthly_count.map(d => d.read_month_fct),
  },
  marks: [
    Plot.ruleY([0]),
    Plot.axisX({label: null, ticks: null, fontFamily: "Inter"}),
    Plot.axisY({label: null, ticks: null, fontFamily: "Inter"}),
    Plot.barY(books.monthly_count, {
      x: "read_month_fct", 
      y: "count", 
      fill: "#f3752f",
      tip: {
        format: {
          x: true,
          y: true
        },
        fontFamily: "Inter"
      }
    })
  ]
})function format_nice_date(date_string) {
  const date = new Date(date_string);
  return new Intl.DateTimeFormat('en-US', { year: 'numeric', month: 'long', day: 'numeric' }).format(date);
}
added_function = aq.addFunction('format_nice_date', format_nice_date)
books_full
  .derive({
    time_actual: d => op.parse_date(d.timestamp),
    pretty_date: d => format_nice_date(d.timestamp),
  })
  .orderby(aq.desc("time_actual"))
  .select({"pretty_date": "Read date", "book_title": "Title", "book_author": "Author", "rating": "Rating"})
  .view()Here’s a login form. It doesn’t actually do anything. But if you needed to generate a JWT token for making POST requests, you could make it do something.