Report rain in whole millimetres; lift tier spans out of the bars (#192)
Two fixes to how measurements read.
Rain goes back to millimetres, rendered as whole numbers — that is how rainfall
is reported, and a tenth of a millimetre is below what the source resolves.
Inches keep two decimals, since a whole inch of rain is a lot to round to.
The distribution strip's low-high span moves from inside each bar to just under
the average, above it. A column is ~38px on a phone, so the old "Max 62° / Min
17°" pill clipped — it had already been shrunk to 8px to cope, and on a
high-rainfall city with longer values it lost both ends of the label, because the
text is centred and overflow-hidden. Above the bar it has the full column width
and needs no scrim to stay legible over nine tier colours.
The span is now bare numbers ("17-62"), since the average directly above it
carries the unit; repeating " g/m³" on both ends is what made it too wide in the
first place. It uses the same precision as that average, or the two lines
disagree ("52mm" over "12.7-136.91"). Sub-1 inch values drop their leading zero
so precip's ten columns still fit a phone.
With nothing inside the bars, the height floor drops from 30px to 14px: it only
has to keep a non-empty bucket visible now, so the heights encode frequency more
honestly. Category names get 2px of side padding, which stops long neighbours
("Light-Mod" beside "Moderate") reading as one word.
Verified at 390/412/1920 in both themes across all four calendar metrics and the
compare strips, in imperial and metric: nothing clips.
Claude-Session: https://claude.ai/code/session_013dRZmX9D3JEntfMKWMTWZ8
This commit is contained in:
parent
77af680895
commit
344721757a
2 changed files with 17 additions and 15 deletions
12
content.py
12
content.py
|
|
@ -69,9 +69,9 @@ _TEMP_METRICS = {"tmax", "tmin", "feels"}
|
||||||
F_COUNTRIES = frozenset({"US", "PR", "GU", "VI", "AS", "MP", "UM",
|
F_COUNTRIES = frozenset({"US", "PR", "GU", "VI", "AS", "MP", "UM",
|
||||||
"BS", "BZ", "KY", "PW", "FM", "MH", "LR"})
|
"BS", "BZ", "KY", "PW", "FM", "MH", "LR"})
|
||||||
|
|
||||||
# One flag drives every measure: a °C page also gets cm and km/h. Mirrors the same
|
# One flag drives every measure: a °C page also gets mm and km/h. Mirrors the same
|
||||||
# decision in frontend/units.js.
|
# decision in frontend/units.js.
|
||||||
CM_PER_IN = 2.54
|
MM_PER_IN = 25.4
|
||||||
KMH_PER_MPH = 1.609344
|
KMH_PER_MPH = 1.609344
|
||||||
|
|
||||||
# The unit the current page renders temperatures in. None = °F, and also means
|
# The unit the current page renders temperatures in. None = °F, and also means
|
||||||
|
|
@ -171,11 +171,13 @@ def _precip(v):
|
||||||
|
|
||||||
|
|
||||||
def _precip_text(v) -> str:
|
def _precip_text(v) -> str:
|
||||||
"""Two decimals either way — cm and inches are only 2.54x apart, so cm needs
|
"""Whole millimetres — that is how rainfall is reported, and a tenth of a
|
||||||
the same resolution, and the source is hundredths of an inch."""
|
millimetre is below what the source resolves. Inches keep two decimals, since
|
||||||
|
a whole inch of rain is a lot to round to."""
|
||||||
if v is None:
|
if v is None:
|
||||||
return "—"
|
return "—"
|
||||||
return f"{v * CM_PER_IN:.2f} cm" if _UNIT.get() == "C" else f"{v:.2f} in"
|
return (f"{_round_half_up(v * MM_PER_IN)} mm" if _UNIT.get() == "C"
|
||||||
|
else f"{v:.2f} in")
|
||||||
|
|
||||||
|
|
||||||
def _wind(v):
|
def _wind(v):
|
||||||
|
|
|
||||||
|
|
@ -304,11 +304,11 @@ def test_units_default_to_the_citys_country(client):
|
||||||
assert 'data-unit-default="F"' in us
|
assert 'data-unit-default="F"' in us
|
||||||
|
|
||||||
|
|
||||||
_PRECIP_SPAN = re.compile(r'<span class="precip" data-precip-in="([\d.]+)">([\d.]+) (in|cm)</span>')
|
_PRECIP_SPAN = re.compile(r'<span class="precip" data-precip-in="([\d.]+)">([\d.]+) (in|mm)</span>')
|
||||||
|
|
||||||
|
|
||||||
def test_precip_and_wind_follow_the_citys_unit(client):
|
def test_precip_and_wind_follow_the_citys_unit(client):
|
||||||
# One toggle drives every measure: a °C page shows cm and km/h, not °C mixed
|
# One toggle drives every measure: a °C page shows mm and km/h, not °C mixed
|
||||||
# with inches and mph. The data-* attribute stays imperial in both, so
|
# with inches and mph. The data-* attribute stays imperial in both, so
|
||||||
# climate.js converts from the same source of truth it does for temperature.
|
# climate.js converts from the same source of truth it does for temperature.
|
||||||
gb = client.get(f"{B}/climate/{SLUG}/records").text # London, GB
|
gb = client.get(f"{B}/climate/{SLUG}/records").text # London, GB
|
||||||
|
|
@ -317,12 +317,12 @@ def test_precip_and_wind_follow_the_citys_unit(client):
|
||||||
gb_p, us_p = _PRECIP_SPAN.findall(gb), _PRECIP_SPAN.findall(us)
|
gb_p, us_p = _PRECIP_SPAN.findall(gb), _PRECIP_SPAN.findall(us)
|
||||||
assert gb_p and us_p
|
assert gb_p and us_p
|
||||||
|
|
||||||
assert {u for _raw, _shown, u in gb_p} == {"cm"}
|
assert {u for _raw, _shown, u in gb_p} == {"mm"}
|
||||||
assert {u for _raw, _shown, u in us_p} == {"in"}
|
assert {u for _raw, _shown, u in us_p} == {"in"}
|
||||||
|
|
||||||
# The conversion is real, not just a relabel.
|
# The conversion is real, not just a relabel.
|
||||||
for raw, shown, _u in gb_p:
|
for raw, shown, _u in gb_p:
|
||||||
assert abs(float(shown) - float(raw) * 2.54) < 0.005, (raw, shown)
|
assert abs(float(shown) - float(raw) * 25.4) < 0.51, (raw, shown)
|
||||||
# On an imperial page the attribute and the text agree.
|
# On an imperial page the attribute and the text agree.
|
||||||
for raw, shown, _u in us_p:
|
for raw, shown, _u in us_p:
|
||||||
assert abs(float(shown) - float(raw)) < 0.005, (raw, shown)
|
assert abs(float(shown) - float(raw)) < 0.005, (raw, shown)
|
||||||
|
|
@ -348,14 +348,14 @@ def test_wind_and_humidity_render_per_unit_system():
|
||||||
|
|
||||||
|
|
||||||
def test_precip_precision_differs_by_unit():
|
def test_precip_precision_differs_by_unit():
|
||||||
# cm and inches are only 2.54x apart, so cm keeps two decimals — at one
|
# Rainfall is reported in whole millimetres; a tenth of a mm is below what
|
||||||
# decimal every light-rain day would collapse to 0.0 or 0.1 cm.
|
# the source resolves. Inches keep two decimals.
|
||||||
import content
|
import content
|
||||||
with content._unit_scope("F"):
|
with content._unit_scope("F"):
|
||||||
assert content._precip_text(0.04) == "0.04 in"
|
assert content._precip_text(0.04) == "0.04 in"
|
||||||
with content._unit_scope("C"):
|
with content._unit_scope("C"):
|
||||||
assert content._precip_text(0.04) == "0.10 cm"
|
assert content._precip_text(0.04) == "1 mm"
|
||||||
assert content._precip_text(1.81) == "4.60 cm"
|
assert content._precip_text(1.81) == "46 mm"
|
||||||
|
|
||||||
|
|
||||||
def test_measure_conversion_constants_match_the_frontend():
|
def test_measure_conversion_constants_match_the_frontend():
|
||||||
|
|
@ -366,9 +366,9 @@ def test_measure_conversion_constants_match_the_frontend():
|
||||||
os.pardir, "frontend", "units.js")
|
os.pardir, "frontend", "units.js")
|
||||||
with open(units_js, encoding="utf-8") as f:
|
with open(units_js, encoding="utf-8") as f:
|
||||||
src = f.read()
|
src = f.read()
|
||||||
cm = float(re.search(r"CM_PER_IN\s*=\s*([\d.]+)", src).group(1))
|
mm = float(re.search(r"MM_PER_IN\s*=\s*([\d.]+)", src).group(1))
|
||||||
kmh = float(re.search(r"KMH_PER_MPH\s*=\s*([\d.]+)", src).group(1))
|
kmh = float(re.search(r"KMH_PER_MPH\s*=\s*([\d.]+)", src).group(1))
|
||||||
assert cm == content.CM_PER_IN
|
assert mm == content.MM_PER_IN
|
||||||
assert kmh == content.KMH_PER_MPH
|
assert kmh == content.KMH_PER_MPH
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue