Travel

FareCalendar

A month of the cheapest fare each day: prices coloured cheap to dear, the lowest day marked, months with fares to move between, and a day to pick.

#travel#flights#calendar#picker#interactive

Demo

Preview

Cheapest fares, November 2026

From $79
Pick a day

Pick a day: on_pick gets it and its fare.

Cheapest fares, December 2026

From 59,90 €
Thursday 10 December · 71,00 €

Get it

It needs theme.qwe, as every component does; or take the whole kit, quillo-ui.qwe. Each component brings its own styles to the pages that show it.

$ curl -O https://quillolang.org/components/ui/theme.qwe$ curl -O https://quillolang.org/components/ui/fare_calendar.qwe
main.qwe
import ui/theme.qwe use theme_cssuse ui/fare_calendar.qweroute GET / = <html><head><style>{theme_css}</style></head><body class="ui-root">...</body></html>

Example

The code of the preview above.

demos/fare_calendar.qwe
// A month and a half of fares, cheaper midweek and dearer at weekends, with a few days without flights.let fare_calendar_demo_prices = {    "2026-11-02": 129.00m, "2026-11-03": 89.00m, "2026-11-04": 94.50m, "2026-11-05": 112.00m, "2026-11-06": 158.00m, "2026-11-07": 171.00m,    "2026-11-08": 149.00m, "2026-11-09": 119.00m, "2026-11-10": 79.00m, "2026-11-11": 84.00m, "2026-11-12": 99.00m, "2026-11-13": 164.00m,    "2026-11-14": 182.00m, "2026-11-15": 140.00m, "2026-11-16": 109.00m, "2026-11-17": 92.00m, "2026-11-18": 92.00m, "2026-11-19": 118.00m,    "2026-11-20": 199.00m, "2026-11-21": 214.00m, "2026-11-22": 176.00m, "2026-11-23": 121.00m, "2026-11-24": 101.00m,    "2026-11-26": 133.00m, "2026-11-27": 245.00m, "2026-11-28": 239.00m, "2026-11-29": 198.00m, "2026-11-30": 115.00m,    "2026-10-30": 99.00m, "2026-10-31": 139.00m,    "2026-12-01": 104.00m, "2026-12-02": 88.00m, "2026-12-03": 97.00m, "2026-12-04": 151.00m, "2026-12-05": 166.00m,}component FareCalendarDemoLive() {    state note = "Pick a day: on_pick gets it and its fare."    <div.demo-stack>        <FareCalendar id="fare-calendar-demo" name="depart" prices={fare_calendar_demo_prices} month="2026-11" today="2026-10-31"                      on_pick={(day, price) => { note = `on_pick: ${day} at ${price}` }}/>        <p.demo-note id="fare-calendar-demo-note">{note}</p>    </div>}view FareCalendarDemo() =    <div class="demo-row" style="align-items: start">        <FareCalendarDemoLive/>        <FareCalendar id="fare-calendar-demo-eur" name="back" prices={{ "2026-12-07": 64.90m, "2026-12-08": 59.90m, "2026-12-10": 71.00m }}                      value="2026-12-10" week_start={0} currency="EUR" locale="de-DE" cents={true}/>    </div>

Source

ui/fare_calendar.qwe
// FareCalendar: the cheapest fare for each day of a month, as a calendar to pick the// day to fly: each day shows its price, coloured as cheap, middling or dear for the// month, the cheapest day marked, days with no flights or already past greyed out, and// the month's lowest fare beside its name. The arrows beside the name go to the months// before and after that have fares. The arrow keys move by day and by week within the// month, and Enter or Space picks. A plain <form> gets the day picked ("YYYY-MM-DD")// as `name`.////   <FareCalendar month="2026-11" prices={{ "2026-11-02": 129.00m, "2026-11-03": 89.00m, "2026-11-04": 94.50m }}//                 today="2026-10-30" name="depart" on_pick={(day, price) => { depart = day }}/>//// prices: a map of "YYYY-MM-DD" to that day's lowest fare (decimals best; numbers are// read as decimals); a day not in it has no flights. month: "YYYY-MM" shown first (the// month of value, else of the first day priced). value: the day picked to start with.// today: the days before it cannot be picked (none by default). week_start: 1 for// Monday (default), 0 for Sunday. currency: ISO code ("USD"); locale: how amounts are// written ("en-US"); cents: false (default) writes whole fares without them ("$89").// name: what a form gets the day as ("date"). label: what screen readers hear before// the month ("Cheapest fares"). on_pick(day, price): inside one of your components,// given the day and its fare (a decimal) when one is picked. id, class.import ui/theme.qwe use ui_clslet fare_calendar_css = css`.ui-farecal { display: grid; gap: 10px; min-width: 0; max-width: 520px; padding: 16px; border: 1px solid var(--ui-border); border-radius: var(--ui-radius);  background: var(--ui-surface); color: var(--ui-fg); font: 14px/1.3 var(--ui-font); font-variant-numeric: tabular-nums; }.ui-farecal-head { display: grid; grid-template-columns: auto minmax(0, 1fr) auto; align-items: center; gap: 8px; }.ui-farecal-title { display: grid; justify-items: center; text-align: center; }.ui-farecal .ui-farecal-month { margin: 0; font-size: 16px; font-weight: 650; }.ui-farecal-from { color: var(--ui-muted); font-size: 12px; }.ui-farecal-from b { color: var(--ui-success); font-weight: 650; }.ui-farecal-nav { display: grid; place-items: center; width: 34px; height: 34px; padding: 0; border: 1px solid var(--ui-border); border-radius: 50%;  background: var(--ui-surface); color: var(--ui-fg); cursor: pointer; }.ui-farecal-nav svg { width: 16px; height: 16px; }.ui-farecal-nav:hover:not(:disabled) { border-color: var(--ui-accent); color: var(--ui-accent); }.ui-farecal-nav:focus-visible { outline: none; box-shadow: var(--ui-focus); }.ui-farecal-nav:disabled { opacity: .35; cursor: not-allowed; }.ui-farecal-grid { display: grid; grid-template-columns: repeat(7, minmax(0, 1fr)); gap: 4px; }.ui-farecal-wd { padding: 2px 0 4px; color: var(--ui-muted); font-size: 11px; font-weight: 600; letter-spacing: .04em; text-align: center; text-transform: uppercase; }.ui-farecal-day { --ui-farecal-k: var(--ui-fg); position: relative; display: grid; align-content: center; justify-items: center; gap: 2px; min-width: 0; height: 54px;  padding: 2px; border: 1px solid var(--ui-border); border-radius: var(--ui-radius-sm); background: var(--ui-surface); color: var(--ui-fg); font: 13px/1.1 var(--ui-font);  cursor: pointer; transition: border-color .12s, background .12s; }.ui-farecal-day b { font-weight: 600; }.ui-farecal-day span { overflow: hidden; max-width: 100%; color: var(--ui-farecal-k); font-size: 11px; font-weight: 600; text-overflow: ellipsis; white-space: nowrap; }.ui-farecal-low { --ui-farecal-k: var(--ui-success); }.ui-farecal-high { --ui-farecal-k: var(--ui-danger); }.ui-farecal-mid { --ui-farecal-k: var(--ui-muted); }.ui-farecal-best { border-color: var(--ui-success); background: var(--ui-success-soft); }.ui-farecal-best::after { content: ""; position: absolute; top: 4px; right: 4px; width: 6px; height: 6px; border-radius: 50%; background: var(--ui-success); }.ui-farecal-day:hover:not([aria-disabled="true"]) { border-color: var(--ui-accent); }.ui-farecal-day:focus-visible { outline: none; box-shadow: var(--ui-focus); }.ui-farecal-day[aria-disabled="true"] { border-color: transparent; background: var(--ui-subtle); color: var(--ui-muted); cursor: not-allowed; }.ui-farecal-day[aria-disabled="true"] span { color: var(--ui-muted); font-weight: 400; }.ui-farecal-day[aria-pressed="true"] { border-color: var(--ui-accent); background: var(--ui-accent); color: var(--ui-accent-fg); }.ui-farecal-day[aria-pressed="true"] span { color: var(--ui-accent-fg); }.ui-farecal-day[aria-pressed="true"]::after { background: var(--ui-accent-fg); }.ui-farecal-today b { text-decoration: underline; text-underline-offset: 2px; }.ui-farecal-foot { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: 6px 14px; }.ui-farecal-key { display: flex; flex-wrap: wrap; gap: 4px 12px; margin: 0; padding: 0; list-style: none; color: var(--ui-muted); font-size: 12px; }.ui-farecal-key li { display: inline-flex; align-items: center; gap: 5px; }.ui-farecal-key li::before { content: ""; width: 8px; height: 8px; border-radius: 50%; background: var(--ui-farecal-k); }.ui-farecal-picked { font-size: 13px; font-weight: 600; }@media (max-width: 420px) { .ui-farecal { padding: 12px 8px; } .ui-farecal-day { height: 48px; } .ui-farecal-day span { font-size: 10px; } }@media (prefers-reduced-motion: reduce) { .ui-farecal-day { transition: none; } }`let fare_calendar_months = ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"]let fare_calendar_days = ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"]// An amount in its currency; cents: false drops the cents of a whole amount ("$89").fn fare_calendar_money(x: dec, currency: str, locale: str, cents: bool) -> str {    let m = money.of(x, currency, "half_up")    if type_of(m) == "error" { return `${format_number(round(x, 2, "half_up"), locale, 2)} ${currency}` }    let text = money.format(m, locale)    let units = money.minor_units(currency)    cents || units == 0 || x != floor(x) ? text : re.replace(text, `[.,]0{${units}}(\\D*)$`, "$1")}// "2026-11" as "November 2026".fn fare_calendar_title(month: str) -> str {    let p = date.parts(`${month}-01`)    `${fare_calendar_months[p.month - 1]} ${p.year}`}// "2026-11-05" as "Thursday 5 November".fn fare_calendar_said(d: str) -> str {    let p = date.parts(d)    `${fare_calendar_days[p.weekday - 1]} ${p.day} ${fare_calendar_months[p.month - 1]}`}component FareCalendar(prices: map, month: str? = null, value: str? = null, today: str? = null, week_start: int = 1, currency: str = "USD",                       locale: str = "en-US", cents: bool = false, name: str = "date", label: str = "Cheapest fares", on_pick = null,                       id: str? = null, class: str? = null) {    style global fare_calendar_css    let days_priced = sort(keys(prices))    let months = days_priced | map((d) => slice(d, 0, 7)) | unique    let first_month = month ?? (value != null ? slice(value, 0, 7) : (months[0] ?? slice(today ?? "2026-01-01", 0, 7)))    state shown = first_month    state picked = value    state focus = value    let base = `fare-calendar-${name}`    let open = (d: str) => prices[d] != null && (today == null || d >= today)    let start = `${shown}-01`    let count = date.days_in_month(start)    let lead_days = week_start == 0 ? date.weekday(start) % 7 : date.weekday(start) - 1    let days = range(1, count + 1) | map((n) => `${shown}-${n < 10 ? `0${n}` : str(n)}`)    let fares = days | filter(open) | map((d) => dec(prices[d]))    let low = min(fares)    let high = max(fares)    let tier = (p: dec) => {        if high == null || high == low { return "low" }        let third = (high - low) / 3        p <= low + third ? "low" : (p >= high - third ? "high" : "mid")    }    let prev = date.add_months(start, -1)    let next = date.add_months(start, 1)    let has_before = any(months, (m) => m < shown)    let has_after = any(months, (m) => m > shown)    let stop = focus != null && slice(focus, 0, 7) == shown ? focus : (days | find(open) ?? days[0])    let names = week_start == 0 ? ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"] : ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"]    let go = (to: str) => {        shown = slice(to, 0, 7)        focus = null    }    let move = (e, by: int) => {        let d = e.target.dataset.day        let to = date.add_days(d, by)        if slice(to, 0, 7) == shown {            e.preventDefault()            let el = e.target.closest(".ui-farecal-grid").querySelector(`[data-day="${to}"]`)            if el != null {                focus = to                el.focus()            }        }    }    let pick = (d: str) => {        if open(d) {            picked = d            focus = d            on_pick && on_pick(d, dec(prices[d]))        }    }    <section class={ui_cls(["ui-farecal", class])} id={id} aria-labelledby={`${base}-month`}>        <div.ui-farecal-head>            <button.ui-farecal-nav type="button" aria-label="Month before" disabled={!has_before} @click={() => go(prev)}>                <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M15 5l-7 7 7 7"/></svg>            </button>            <div.ui-farecal-title>                <h3.ui-farecal-month id={`${base}-month`} aria-live="polite"><span.ui-sr>{label}, </span>{fare_calendar_title(shown)}</h3>                <span.ui-farecal-from>{low != null ? ["From ", <b>{fare_calendar_money(low, currency, locale, cents)}</b>] : "No fares this month"}</span>            </div>            <button.ui-farecal-nav type="button" aria-label="Month after" disabled={!has_after} @click={() => go(next)}>                <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M9 5l7 7-7 7"/></svg>            </button>        </div>        <div.ui-farecal-grid role="group" aria-labelledby={`${base}-month`}>            {for w in names => <span.ui-farecal-wd aria-hidden="true">{w}</span>}            {for i in range(lead_days) => <span aria-hidden="true"></span>}            {for d in days => {                let ok = open(d)                let p = prices[d] != null ? dec(prices[d]) : null                let t = ok ? tier(p) : null                let best = ok && p == low                let price_text = p != null ? fare_calendar_money(p, currency, locale, cents) : null                let said = join([fare_calendar_said(d), ok ? price_text : (p != null ? "past" : "no flights"), best ? "lowest fare" : null] | filter((x) => x != null), ", ")                <button class={ui_cls(["ui-farecal-day", t != null && `ui-farecal-${t}`, best && "ui-farecal-best", d == today && "ui-farecal-today"])}                        key={d} type="button" data-day={d} aria-label={said} aria-pressed={d == picked ? "true" : "false"}                        aria-disabled={ok ? null : "true"} tabindex={d == stop ? "0" : "-1"}                        @click={() => pick(d)}                        @keydown={(e) => {                            match e.key {                                "ArrowRight" => move(e, 1)                                "ArrowLeft" => move(e, -1)                                "ArrowDown" => move(e, 7)                                "ArrowUp" => move(e, -7)                                _ => null                            }                        }}>                    <b>{int(slice(d, 8, 10))}</b>                    {" "}<span aria-hidden="true">{ok ? price_text : "–"}</span>                </button>            }}        </div>        <div.ui-farecal-foot>            <ul.ui-farecal-key aria-hidden="true">                <li.ui-farecal-low>Cheap</li><li.ui-farecal-mid>Usual</li><li.ui-farecal-high>Dear</li>            </ul>            <span.ui-farecal-picked aria-live="polite">                {picked != null ? `${fare_calendar_said(picked)} · ${fare_calendar_money(dec(prices[picked] ?? 0), currency, locale, cents)}` : "Pick a day"}            </span>        </div>        <input type="hidden" name={name} value={picked ?? ""}/>    </section>}
Thread
88Useful? Like it, or add to its thread below.

Thread 0 posts

On FareCalendar: questions, tips, corrections, and code that uses it.

No posts yet. The first one opens the thread.