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.