Compare distribution bars: Max/Min range rows + non-overlapping labels (#86)
* Distribution bars: split the in-bar range into Max/Min rows The min–max range inside each distribution bar packed onto one line (e.g. -20–-16°), where the range dash collides with the minus signs and becomes unreadable for negative temperatures. Split it into two labelled rows — Max on top, Min below — so each value stands alone. The per-tier average above each bar is unchanged. Shared by the Compare distribution and the Calendar totals strip. Bump the bar-height floor 26→30px so the two-line label never clips. * Compare: nudge overlapping distribution-bar labels apart on desktop The desktop key positioned each bucket label at its segment's midpoint, so two small adjacent buckets (e.g. Warm 1% next to Too hot 2%) rendered their labels on top of each other. Add a post-layout pass that measures each label and sweeps the row — push right to clear the previous label, clamp the tail to the bar's right edge, then pull left — so no two overlap while each stays as close to its segment as the space allows. Re-runs on resize; mobile keeps its plain wrapping row.
This commit is contained in:
parent
3fbaffd876
commit
a88bb4f2e6
1 changed files with 46 additions and 0 deletions
|
|
@ -520,6 +520,44 @@ function renderResults() {
|
||||||
|
|
||||||
results.innerHTML = 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("") : "");
|
(loading ? Array.from({ length: loading }, skeletonRow).join("") : "");
|
||||||
|
spreadKeys();
|
||||||
|
}
|
||||||
|
|
||||||
|
// The desktop key positions each label at its segment's midpoint, so two small
|
||||||
|
// adjacent buckets (e.g. Warm 1% right next to Too hot 2%) render their labels on
|
||||||
|
// top of each other. After layout, measure each label and sweep the row so no two
|
||||||
|
// overlap: push right to clear the previous label, clamp the tail to the right edge,
|
||||||
|
// then pull left — keeping every label as close to its segment as the space allows.
|
||||||
|
// Mobile uses a plain wrapping row, so there we just clear any positions we applied.
|
||||||
|
function spreadKeys() {
|
||||||
|
const desktop = window.matchMedia("(min-width: 641px)").matches;
|
||||||
|
results.querySelectorAll(".cmp-key").forEach((keyEl) => {
|
||||||
|
const items = [...keyEl.children];
|
||||||
|
if (!desktop) {
|
||||||
|
items.forEach((el) => { el.style.left = el.style.right = el.style.transform = ""; });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (items.length < 2) return;
|
||||||
|
const W = keyEl.clientWidth, gap = 10;
|
||||||
|
// Ideal centre (px) for each label, honouring the end anchors (first pinned to
|
||||||
|
// the left edge, last to the right); middles sit at their inline midpoint %.
|
||||||
|
const m = items.map((el, i) => {
|
||||||
|
const hw = el.offsetWidth / 2;
|
||||||
|
const c = i === 0 ? hw
|
||||||
|
: i === items.length - 1 ? W - hw
|
||||||
|
: (parseFloat(el.style.left) / 100) * W;
|
||||||
|
return { el, hw, c };
|
||||||
|
});
|
||||||
|
for (let i = 1; i < m.length; i++) // forward: clear the previous label
|
||||||
|
m[i].c = Math.max(m[i].c, m[i - 1].c + m[i - 1].hw + gap + m[i].hw);
|
||||||
|
m[m.length - 1].c = Math.min(m[m.length - 1].c, W - m[m.length - 1].hw);
|
||||||
|
for (let i = m.length - 2; i >= 0; i--) // backward: pull back within the edge
|
||||||
|
m[i].c = Math.min(m[i].c, m[i + 1].c - m[i + 1].hw - gap - m[i].hw);
|
||||||
|
m[0].c = Math.max(m[0].c, m[0].hw); // keep the first inside the left edge
|
||||||
|
m.forEach(({ el, c }) => {
|
||||||
|
el.style.left = `${c}px`; el.style.right = "auto"; el.style.transform = "translateX(-50%)";
|
||||||
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---- distribution (independent of the comfort filter) ----
|
// ---- distribution (independent of the comfort filter) ----
|
||||||
|
|
@ -605,6 +643,14 @@ thiInput.addEventListener("input", () => { thi = Math.max(+thiInput.value, hi);
|
||||||
// (its per-category averages + ranges are in the active unit).
|
// (its per-category averages + ranges are in the active unit).
|
||||||
onUnitChange(() => { syncSlicer(); renderResults(); renderDist(); });
|
onUnitChange(() => { syncSlicer(); renderResults(); renderDist(); });
|
||||||
|
|
||||||
|
// Label widths (and the desktop/mobile threshold) move with the viewport, so
|
||||||
|
// re-spread the rank-card keys after the layout settles.
|
||||||
|
let spreadRaf = 0;
|
||||||
|
window.addEventListener("resize", () => {
|
||||||
|
cancelAnimationFrame(spreadRaf);
|
||||||
|
spreadRaf = requestAnimationFrame(spreadKeys);
|
||||||
|
});
|
||||||
|
|
||||||
// Distribution metric: an inline selector (by the chart) plus a synced copy in the
|
// Distribution metric: an inline selector (by the chart) plus a synced copy in the
|
||||||
// filter sheet. Either one drives the shared distMetric; both reflect the active state.
|
// filter sheet. Either one drives the shared distMetric; both reflect the active state.
|
||||||
const syncDistButtons = () =>
|
const syncDistButtons = () =>
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue