Compare: temperature markers over slider handles; split range labels (#75)

Make the comfort/tolerance slider self-explanatory and fix its labeling.

- A temperature marker sits above each of the four handles (the band
  borders) — the two comfort bounds and the two tolerance bounds — so the
  numbers read straight off the track instead of a combined caption.
- The header is now two disparate labels, "Comfort lo–hi" and "Tolerance
  tlo–thi", rather than the old "Comfort & tolerance 16–22 · tolerance
  4–27" that mislabeled the comfort range.
- Distribution-bar key: on desktop each category label is positioned at its
  segment's midpoint (ends pinned to the bar edges) so the labels line up
  under the colors they describe; on mobile it stays a plain wrapping row.
This commit is contained in:
Emi Griffith 2026-07-11 19:23:28 -07:00 committed by GitHub
parent 9f3c2caabc
commit bc1aac3424
3 changed files with 51 additions and 6 deletions

View file

@ -59,7 +59,16 @@
the date range or the location set trigger a data load. -->
<div id="cmp-params" class="cmp-params" hidden>
<div class="cmp-param cmp-comfort">
<label>Comfort &amp; tolerance <b id="cmp-range-val"></b></label>
<div class="cmp-range-labels">
<span class="cmp-rl cmp-rl-comfort">Comfort <b id="cmp-comfort-val"></b></span>
<span class="cmp-rl cmp-rl-tol">Tolerance <b id="cmp-tol-val"></b></span>
</div>
<div class="cmp-ticks" id="cmp-ticks" aria-hidden="true">
<span class="cmp-tick tol" id="cmp-tick-tlo"></span>
<span class="cmp-tick" id="cmp-tick-lo"></span>
<span class="cmp-tick" id="cmp-tick-hi"></span>
<span class="cmp-tick tol" id="cmp-tick-thi"></span>
</div>
<div class="dual-range quad-range" id="cmp-slicer">
<div class="dual-track">
<span class="dual-fill dual-fill-tol" id="cmp-fill-tol"></span>
@ -70,7 +79,7 @@
<input id="cmp-hi" type="range" min="30" max="100" step="1" aria-label="Comfort range upper bound" />
<input id="cmp-thi" type="range" min="30" max="100" step="1" class="thumb-tol" aria-label="Tolerance range upper bound" />
</div>
<span class="hint">Inner handles are your ideal comfort range; outer handles are the tolerance range — days there still count, ramped by how far off they land.</span>
<span class="hint">Inner handles set your comfort range; outer handles set the tolerance range. Days in tolerance still count toward the score, scaled by how far past comfort they fall.</span>
</div>
<div class="cmp-param cmp-basis-block">

View file

@ -151,7 +151,12 @@ const loInput = document.getElementById("cmp-lo");
const hiInput = document.getElementById("cmp-hi");
const tloInput = document.getElementById("cmp-tlo");
const thiInput = document.getElementById("cmp-thi");
const rangeVal = document.getElementById("cmp-range-val");
const comfortValEl = document.getElementById("cmp-comfort-val");
const tolValEl = document.getElementById("cmp-tol-val");
const tickEls = {
tlo: document.getElementById("cmp-tick-tlo"), lo: document.getElementById("cmp-tick-lo"),
hi: document.getElementById("cmp-tick-hi"), thi: document.getElementById("cmp-tick-thi"),
};
const fillEl = document.getElementById("cmp-fill");
const fillTolEl = document.getElementById("cmp-fill-tol");
const basisToggle = document.getElementById("cmp-basis");
@ -369,9 +374,17 @@ function rankCard(loc, s, rank) {
parts.map((p) => `<span class="cmp-seg ${p.cls}" style="width:${p.w}%" title="${p.label}${pct(p.w)}"></span>`).join("") +
`</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.
// the same cold→hot order as the bar, so small spans are still clearly labeled. On
// desktop each label is positioned at its segment's midpoint (the ends anchored to
// the bar edges); on mobile it falls back to a plain wrapping row.
let acc = 0;
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("") +
parts.map((p, i) => {
const mid = acc + p.w / 2; acc += p.w;
const pos = i === 0 ? "left:0" : i === parts.length - 1 ? "right:0" : `left:${mid}%`;
const edge = i === 0 ? " cmp-key-first" : i === parts.length - 1 ? " cmp-key-last" : "";
return `<span class="cmp-key-item${edge}" style="${pos}"><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`;
@ -508,11 +521,17 @@ locList.addEventListener("click", (e) => {
const pctPos = (v) => (v - R_MIN) / (R_MAX - R_MIN) * 100;
function syncSlicer() {
tloInput.value = tlo; loInput.value = lo; hiInput.value = hi; thiInput.value = thi;
rangeVal.textContent = `${fmtTemp(lo)}${fmtTemp(hi)} · tolerance ${fmtTemp(tlo)}${fmtTemp(thi)}`;
comfortValEl.textContent = `${fmtTemp(lo)}${fmtTemp(hi)}`;
tolValEl.textContent = `${fmtTemp(tlo)}${fmtTemp(thi)}`;
fillEl.style.left = `${pctPos(lo)}%`;
fillEl.style.right = `${100 - pctPos(hi)}%`;
fillTolEl.style.left = `${pctPos(tlo)}%`;
fillTolEl.style.right = `${100 - pctPos(thi)}%`;
// A temperature marker above each handle (the four band borders).
for (const [k, v] of [["tlo", tlo], ["lo", lo], ["hi", hi], ["thi", thi]]) {
tickEls[k].style.left = `${pctPos(v)}%`;
tickEls[k].textContent = fmtTemp(v);
}
}
function slicerChanged() {
lsSet(CMP.lo, String(lo)); lsSet(CMP.hi, String(hi));

View file

@ -864,6 +864,14 @@ main { max-width: 1200px; margin: 0 auto; padding: 20px 24px 60px; }
.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; }
/* Desktop: spread the labels across the bar's width, each centred under its own
segment (first/last pinned to the ends so they never spill past the edge). */
@media (min-width: 641px) {
.cmp-key { position: relative; display: block; height: 18px; }
.cmp-key-item { position: absolute; transform: translateX(-50%); white-space: nowrap; }
.cmp-key-first { transform: none; }
.cmp-key-last { transform: none; }
}
.cmp-bands { margin: 8px 0 0; font-size: 12.5px; color: var(--muted); }
.cmp-bands b { color: var(--text); }
@ -922,6 +930,15 @@ main { max-width: 1200px; margin: 0 auto; padding: 20px 24px 60px; }
.cmp-skel-badge, .cmp-skel-name, .cmp-skel-big, .cmp-skel-bar { animation: none; }
}
/* Comfort vs tolerance shown as two separate labels, and a temperature marker above
each of the four handles (the band borders). */
.cmp-range-labels { display: flex; flex-wrap: wrap; gap: 4px 18px; margin: 0 0 6px; font-size: 13px; color: var(--muted); }
.cmp-rl b { color: var(--text); font-size: 15px; margin-left: 2px; }
.cmp-rl-comfort b { color: var(--accent); }
.cmp-ticks { position: relative; height: 17px; }
.cmp-tick { position: absolute; transform: translateX(-50%); font-size: 11px; font-weight: 700; color: var(--text); white-space: nowrap; }
.cmp-tick.tol { color: var(--muted); font-weight: 600; }
/* Dual-handle comfort slicer: two range inputs overlaid on a shared track, with a
fill marking the selected span. Only the thumbs take pointer events, so both
handles stay grabbable even where the inputs overlap. */