Compare: label every bar segment with a key; drop the colored caption (#73)

The bar showed each segment's share but not what it meant, leaning on the
coloured below/above-comfort text to explain it. Replace that text with a
per-card key directly under the bar: a colour swatch + plain name + share for
every bucket present (Too cold / Cool / Comfortable / Warm / Too hot), in the
same cold→hot order as the bar. Every span is spelled out, so a 1% sliver is
as clear as a 60% block, and small segments no longer need to fit a label
inside them. Bar segments also carry a hover title. The redundant top legend
is removed (each card now names its own colours), and the leftover summary
line keeps just the tolerance share and typical miss.
This commit is contained in:
Emi Griffith 2026-07-11 18:49:22 -07:00 committed by GitHub
parent 46c5006ce0
commit 9f3c2caabc
2 changed files with 38 additions and 50 deletions

View file

@ -331,17 +331,6 @@ const pct = (v) => `${v < 10 ? v.toFixed(1) : Math.round(v)}%`;
// Temperature *differences* (avg miss, typical miss) in the active unit.
const deg = (v) => fmtDelta(v);
// Names the five bar colors once, above the ranked cards.
const LEGEND =
`<div class="cmp-legend" aria-hidden="true">` +
`<span><i class="cmp-sw cmp-seg-vcold"></i>Too cold</span>` +
`<span><i class="cmp-sw cmp-seg-cool"></i>Cool</span>` +
`<span><i class="cmp-sw cmp-seg-comf"></i>Comfortable</span>` +
`<span><i class="cmp-sw cmp-seg-warm"></i>Warm</span>` +
`<span><i class="cmp-sw cmp-seg-vhot"></i>Too hot</span>` +
`<span class="cmp-legend-note">each bar = 100% of days · “cool”/“warm” fall within your tolerance</span>` +
`</div>`;
function renderLocList() {
locList.innerHTML = locations.map((l, i) => {
let label, cls = "cmp-chip", lead = "";
@ -358,29 +347,33 @@ function renderLocList() {
}).join("");
}
// The five day-buckets, ordered cold → hot: css color class, plain-language name,
// and the stat key holding its share of days. Drives both the bar and its key.
const BUCKETS = [
["cmp-seg-vcold", "Too cold", "belowOutPct"],
["cmp-seg-cool", "Cool", "belowTolPct"],
["cmp-seg-comf", "Comfortable", "comfortPct"],
["cmp-seg-warm", "Warm", "aboveTolPct"],
["cmp-seg-vhot", "Too hot", "aboveOutPct"],
];
// A ranked place: the 0100 match score up top, then one full-width bar showing how
// this place's days split across five temperature buckets, ordered cold → hot using
// the site's own temperature colors — too cold, cool (within tolerance), comfortable,
// warm (within tolerance), too hot. The whole bar is 100% of the days, so every
// segment is a real share and a place that runs cold vs one that runs hot read at a
// glance (more blue vs more red). The legend above the cards names each color.
// the place's days split across the five temperature buckets (cold → hot, in the
// site's own colors), and directly beneath it a key that names every bucket and its
// share — so even a 1% sliver on the bar reads clearly. The whole bar is 100% of the
// days, so more blue = runs cold, more red = runs hot.
function rankCard(loc, s, rank) {
const win = rank === 1;
// Label a segment inline when it's wide enough to fit the number legibly.
const seg = (cls, w) => (w > 0
? `<span class="cmp-seg ${cls}" style="width:${w}%">${w >= 10 ? `<span class="cmp-seg-n">${Math.round(w)}%</span>` : ""}</span>`
: "");
const bar = `<div class="cmp-bar" role="img" aria-label="of all days: ${pct(s.belowOutPct)} too cold, ${pct(s.belowTolPct)} cool, ${pct(s.comfortPct)} comfortable, ${pct(s.aboveTolPct)} warm, ${pct(s.aboveOutPct)} too hot">` +
seg("cmp-seg-vcold", s.belowOutPct) + seg("cmp-seg-cool", s.belowTolPct) +
seg("cmp-seg-comf", s.comfortPct) +
seg("cmp-seg-warm", s.aboveTolPct) + seg("cmp-seg-vhot", s.aboveOutPct) +
const parts = BUCKETS.map(([cls, label, key]) => ({ cls, label, w: s[key] })).filter((p) => p.w > 0);
const bar = `<div class="cmp-bar" role="img" aria-label="of all days: ${parts.map((p) => `${pct(p.w)} ${p.label.toLowerCase()}`).join(", ")}">` +
parts.map((p) => `<span class="cmp-seg ${p.cls}" style="width:${p.w}%" title="${p.label}${pct(p.w)}"></span>`).join("") +
`</div>`;
const cap =
`<div class="cmp-cap-row">` +
`<span class="cmp-cap-below">${pct(s.belowPct)} below comfort${s.below ? ` · avg ${deg(s.avgBelow)} colder` : ""}</span>` +
`<span class="cmp-cap-above">${s.above ? `avg ${deg(s.avgAbove)} hotter · ` : ""}${pct(s.abovePct)} above comfort</span>` +
`</div>`;
const bands = `<div class="cmp-bands"><b>${pct(s.comfortPct)}</b> comfortable · <b>${pct(s.tolPct)}</b> within tolerance · typically <b>${deg(s.mad)}</b> off</div>`;
// Key under the bar: a color swatch + name + share for every non-empty bucket, in
// the same cold→hot order as the bar, so small spans are still clearly labeled.
const key = `<div class="cmp-key">` +
parts.map((p) => `<span class="cmp-key-item"><i class="cmp-sw ${p.cls}"></i>${p.label} <b>${pct(p.w)}</b></span>`).join("") +
`</div>`;
const bands = `<div class="cmp-bands"><b>${pct(s.tolPct)}</b> within tolerance · typically <b>${deg(s.mad)}</b> off comfort</div>`;
const sub = win ? `<span class="cmp-best">★ Best match</span> · ${s.n} days` : `${s.n} days`;
return `<div class="cmp-card${win ? " cmp-win" : ""}">` +
`<div class="cmp-card-top">` +
@ -388,7 +381,7 @@ function rankCard(loc, s, rank) {
`<div class="cmp-card-name"><span class="cmp-name">${loc.name}</span><span class="cmp-daycount">${sub}</span></div>` +
`<div class="cmp-big"><b>${Math.round(s.score)}</b><span>match / 100</span></div>` +
`</div>` +
bar + cap + bands +
bar + key + bands +
`</div>`;
}
@ -463,7 +456,7 @@ function renderResults() {
`<p class="meta">By ${BASIS_LABEL[basis]} · comfort ${fmtTemp(lo)}${fmtTemp(hi)}, tolerance ${fmtTemp(tlo)}${fmtTemp(thi)} · ${span}` +
`${loading ? ` · <span class="cmp-loading-inline"><span class="cmp-spinner"></span>loading ${loading} more…</span>` : ""}</p>`;
results.innerHTML = LEGEND + scored.map((x, i) => rankCard(x.loc, x.s, i + 1)).join("") +
results.innerHTML = scored.map((x, i) => rankCard(x.loc, x.s, i + 1)).join("") +
(loading ? Array.from({ length: loading }, skeletonRow).join("") : "");
}

View file

@ -847,28 +847,24 @@ main { max-width: 1200px; margin: 0 auto; padding: 20px 24px 60px; }
/* Diverging comfort bar: a fixed center marks the comfort zone; colder days grow
left of it, warmer days grow right. Both halves share one 0100% scale, so a
cold-skewed place and a hot-skewed one mirror each other and compare at a glance. */
/* Legend naming the five bar colors once, above the cards. */
.cmp-legend { display: flex; flex-wrap: wrap; align-items: center; gap: 6px 14px; margin: 2px 0 4px; font-size: 12px; color: var(--muted); }
.cmp-legend > span { display: inline-flex; align-items: center; gap: 6px; }
.cmp-sw { width: 12px; height: 12px; border-radius: 3px; flex: 0 0 auto; }
.cmp-legend-note { font-style: italic; opacity: .85; }
/* Full-width day-distribution bar: five temperature buckets, cold hot, together
100% of the days, in the site's own temperature colors. Every segment is a real
share, so more blue = runs cold, more red = runs hot no empty space to decode. */
.cmp-bar { display: flex; height: 22px; border-radius: 6px; overflow: hidden; margin: 14px 0 8px; background: var(--surface-2); }
.cmp-seg { height: 100%; display: flex; align-items: center; justify-content: center; min-width: 0; }
.cmp-seg-n { font-size: 10.5px; font-weight: 700; color: rgba(10, 15, 20, .68); }
.cmp-seg-vcold .cmp-seg-n, .cmp-seg-comf .cmp-seg-n, .cmp-seg-vhot .cmp-seg-n { color: rgba(255, 255, 255, .95); }
/* Full-width day-distribution bar: five temperature buckets, cold to hot, together
100% of the days, in the site's own temperature colors. min-width keeps a thin
sliver visible; the key below spells out every color and share. */
.cmp-bar { display: flex; height: 20px; border-radius: 6px; overflow: hidden; margin: 14px 0 10px; background: var(--surface-2); }
.cmp-seg { height: 100%; min-width: 2px; }
.cmp-seg-vcold { background: var(--very-cold); }
.cmp-seg-cool { background: var(--cool); }
.cmp-seg-comf { background: var(--normal); }
.cmp-seg-warm { background: var(--warm); }
.cmp-seg-vhot { background: var(--very-hot); }
.cmp-cap-row { display: flex; justify-content: space-between; gap: 10px; font-size: 12px; }
.cmp-cap-below { color: var(--cold); }
.cmp-cap-above { color: var(--hot); text-align: right; }
/* Key under the bar: swatch + plain name + share for every bucket the place has, in
the same cold-to-hot order as the bar so a 1% span is named as clearly as a 60% one. */
.cmp-key { display: flex; flex-wrap: wrap; gap: 6px 14px; font-size: 12.5px; color: var(--muted); }
.cmp-key-item { display: inline-flex; align-items: center; gap: 6px; }
.cmp-key-item b { color: var(--text); font-weight: 700; }
.cmp-sw { width: 12px; height: 12px; border-radius: 3px; flex: 0 0 auto; }
.cmp-bands { margin: 8px 0 0; font-size: 12.5px; color: var(--muted); }
.cmp-bands b { color: var(--text); }
@ -1065,6 +1061,5 @@ main { max-width: 1200px; margin: 0 auto; padding: 20px 24px 60px; }
.cmp-bl-row { grid-template-columns: 84px 1fr; gap: 8px; }
.cmp-bl-name { font-size: 12px; }
.cmp-bl-axis { margin-left: 92px; }
.cmp-cap-row { font-size: 11px; }
.cmp-legend { font-size: 11px; gap: 5px 10px; }
.cmp-key { font-size: 11.5px; gap: 5px 12px; }
}