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. --> the date range or the location set trigger a data load. -->
<div id="cmp-params" class="cmp-params" hidden> <div id="cmp-params" class="cmp-params" hidden>
<div class="cmp-param cmp-comfort"> <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-range quad-range" id="cmp-slicer">
<div class="dual-track"> <div class="dual-track">
<span class="dual-fill dual-fill-tol" id="cmp-fill-tol"></span> <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-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" /> <input id="cmp-thi" type="range" min="30" max="100" step="1" class="thumb-tol" aria-label="Tolerance range upper bound" />
</div> </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>
<div class="cmp-param cmp-basis-block"> <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 hiInput = document.getElementById("cmp-hi");
const tloInput = document.getElementById("cmp-tlo"); const tloInput = document.getElementById("cmp-tlo");
const thiInput = document.getElementById("cmp-thi"); 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 fillEl = document.getElementById("cmp-fill");
const fillTolEl = document.getElementById("cmp-fill-tol"); const fillTolEl = document.getElementById("cmp-fill-tol");
const basisToggle = document.getElementById("cmp-basis"); 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("") + parts.map((p) => `<span class="cmp-seg ${p.cls}" style="width:${p.w}%" title="${p.label}${pct(p.w)}"></span>`).join("") +
`</div>`; `</div>`;
// Key under the bar: a color swatch + name + share for every non-empty bucket, in // 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">` + 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>`; `</div>`;
const bands = `<div class="cmp-bands"><b>${pct(s.tolPct)}</b> within tolerance · typically <b>${deg(s.mad)}</b> off comfort</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`; 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; const pctPos = (v) => (v - R_MIN) / (R_MAX - R_MIN) * 100;
function syncSlicer() { function syncSlicer() {
tloInput.value = tlo; loInput.value = lo; hiInput.value = hi; thiInput.value = thi; 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.left = `${pctPos(lo)}%`;
fillEl.style.right = `${100 - pctPos(hi)}%`; fillEl.style.right = `${100 - pctPos(hi)}%`;
fillTolEl.style.left = `${pctPos(tlo)}%`; fillTolEl.style.left = `${pctPos(tlo)}%`;
fillTolEl.style.right = `${100 - pctPos(thi)}%`; 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() { function slicerChanged() {
lsSet(CMP.lo, String(lo)); lsSet(CMP.hi, String(hi)); 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 { display: inline-flex; align-items: center; gap: 6px; }
.cmp-key-item b { color: var(--text); font-weight: 700; } .cmp-key-item b { color: var(--text); font-weight: 700; }
.cmp-sw { width: 12px; height: 12px; border-radius: 3px; flex: 0 0 auto; } .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 { margin: 8px 0 0; font-size: 12.5px; color: var(--muted); }
.cmp-bands b { color: var(--text); } .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; } .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 /* 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 fill marking the selected span. Only the thumbs take pointer events, so both
handles stay grabbable even where the inputs overlap. */ handles stay grabbable even where the inputs overlap. */