Skip to content

Commit 3cafc37

Browse files
Mobile UX polish, finish v2 SEO/OG, smooth Material Source curve
Mobile sliders - Multi-row vertical layout (label | slider | info-row) that mirrors desktop; no horizontal-overlap concerns. - Slider track centered, fixed-but-responsive width (min(60vw, 220px)) so every row has the same length and slider edges align across ingredients. - Names left-aligned (consistent with the 'min' bound just below). - Smaller 16px thumb on mobile; --thumb-half CSS var keeps the JS slider-preview-marker math in sync with the rendered thumb size. - Vertical alignment of name and value-input via 'align-items: center' on the label flex container. - Mobile font hierarchy bumped for legibility on touch devices and older eyes: label 0.85rem, panel-body insight text 0.95rem, h2 0.95rem. Hide W/B readout on mobile to keep GWP+Cost on one line. - Replaced ingredient-name 'border-bottom: 1px dashed' with 'text-decoration: underline dashed' to save a few pixels of vertical height per row (no descender expansion). Click-to-edit composition values - Type a number + Enter, blur, or unit-toggle commit; Escape reverts; out-of-range clamps; non-numeric reverts. Material Source value is a <span> (non-editable). Pre-toggle unit context preserved on commit. - 'Temp' renamed to 'Temperature' in displayed labels (column name unchanged so the model layer is untouched). Material Source toggle - Curve-level 350ms ease-in-out blend between pre/post-toggle posteriors in drawStrengthCurve (avoids feeding fractional categorical input to the GP). State exposed via window.__test for deterministic testing. - scheduleInsightUpdate() on toggle so the Mix Insight no longer goes stale when the new composition is outside the training set. Finish v2: SEO + social - robots.txt and sitemap.xml. - README link to the deployed explorer. - (Open Graph / Twitter Card / JSON-LD already in head.) Bug fixes - Filter min/max placeholders no longer clipped by Chromium spinner UI ('appearance: textfield' + zeroed -webkit spin pseudo-elements). - Slider preview marker now offsets by slider.offsetLeft so it lands on the visible track on mobile (and stays a no-op on desktop). - displayPreviewComp now stays in sync with currentComposition on Material Source toggle (no more dashed-preview ghosting). Refactor / cleanup - logSpacedTimes() and computeStds() helpers consolidate three duplicated copies of the time-grid generator and the GP-noise std computation. - Removed pre-existing dead declarations in updateMixInsight and getParetoTag. - Renamed unused @parameterized.expand 'name' params to '_name' in test/test_utils.py so the linter no longer flags them. Tests (10 new specs + extended sliders) - preview-curve: displayPreviewComp sync, mix-insight refresh on toggle, curve-transition state hook (deterministic, no screenshot timing race). - font-uniformity: insight bodies share computed font-size. - og-meta + seo: required tags present, og-image fetch in budget, robots.txt and sitemap.xml reachable. - mobile-slider-layout: multi-row no overlap, names left-aligned, uniform slider widths, centered+narrower slider, vertical centerline, text-decoration affordance, font hierarchy, value glyph aligns with max bound, tap target via offsetHeight, preview marker on track. - mobile-value-fit: no panel overflow at default, fractional, imperial, imperial-max, and 320px viewports. - scatter-filter: placeholders fit fully (no spinner clip). - readouts-strip: desktop has all three, mobile hides W/B and stays on one line. - sliders.spec: extended with 7 click-to-edit cases. - test/e2e/README.md invariants table updated.
1 parent e6a189c commit 3cafc37

18 files changed

Lines changed: 1603 additions & 63 deletions

README.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
# BOxCrete: Bayesian Optimization for Sustainable Concrete Mix Design
22

3-
Concrete, the second most widely used material in the world, accounts for **6–8% of global anthropogenic CO₂ emissions**, largely due to Portland cement production (~0.8 tons CO₂ per ton of cement). Partial replacement with Supplementary Cementitious Materials (SCMs) such as fly ash, slag, and natural pozzolan reduces embodied carbon and often improves durability, but high SCM usage makes compressive strength a highly nonlinear function of multiple interacting mix parameters, rendering traditional design empirical and trial-and-error driven. To systematically navigate this complex composition space, data-driven frameworks are needed.
3+
> 🌐 **[Try the interactive explorer →](https://facebookresearch.github.io/SustainableConcrete/)** — predict concrete strength from mix composition in your browser, no installation needed.
4+
5+
Concrete, the second most widely used material in the world, accounts for **6–8% of global anthropogenic CO₂ emissions**, largely due to Portland cement production (~0.8 tons CO₂ per ton of cement).
46
Here, we introduce BOxCrete, an open-source Bayesian optimization framework for probabilistic strength curve prediction and sustainable mix design.
57
We invite researchers and practitioners from all disciplines including AI, machine learning, computer science, materials science, and civil engineering
68
to collaborate on discovering more sustainable concrete formulations that are applicable

docs/index.html

Lines changed: 66 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,71 @@
55
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">
66
<title>BOxCrete – Interactive Concrete Strength Explorer</title>
77
<meta name="theme-color" content="#e8edf3">
8+
9+
<!-- SEO -->
10+
<meta name="description" content="Interactive Bayesian Optimization explorer for sustainable concrete: predict 28-day compressive strength, GWP, and cost from mix composition in your browser.">
11+
<meta name="author" content="Sebastian Ament; BOxCrete authors">
12+
<link rel="canonical" href="https://facebookresearch.github.io/SustainableConcrete/">
13+
14+
<!-- Open Graph (Facebook, LinkedIn, Slack, iMessage, Discord) -->
15+
<meta property="og:type" content="website">
16+
<meta property="og:site_name" content="BOxCrete">
17+
<meta property="og:title" content="BOxCrete – Interactive Concrete Strength Explorer">
18+
<meta property="og:description" content="Predict concrete compressive strength, GWP, and cost from mix composition. A Bayesian Optimization model running entirely in your browser.">
19+
<meta property="og:url" content="https://facebookresearch.github.io/SustainableConcrete/">
20+
<meta property="og:image" content="https://facebookresearch.github.io/SustainableConcrete/og-image.jpg">
21+
<meta property="og:image:width" content="1200">
22+
<meta property="og:image:height" content="630">
23+
<meta property="og:image:alt" content="BOxCrete: a close-up photograph of fresh concrete, used as the social-share thumbnail for the interactive explorer.">
24+
25+
<!-- Twitter / X Card -->
26+
<meta name="twitter:card" content="summary_large_image">
27+
<meta name="twitter:title" content="BOxCrete – Interactive Concrete Strength Explorer">
28+
<meta name="twitter:description" content="Predict concrete compressive strength, GWP, and cost from mix composition. A Bayesian Optimization model running entirely in your browser.">
29+
<meta name="twitter:image" content="https://facebookresearch.github.io/SustainableConcrete/og-image.jpg">
30+
31+
<!-- iOS share sheet uses apple-touch-icon as a fallback thumbnail -->
32+
<link rel="apple-touch-icon" href="og-image.jpg">
33+
34+
<!-- Schema.org structured data: WebApplication + scholarly citations -->
35+
<script type="application/ld+json">
36+
{
37+
"@context": "https://schema.org",
38+
"@type": "WebApplication",
39+
"name": "BOxCrete",
40+
"alternateName": "BOxCrete Interactive Concrete Strength Explorer",
41+
"url": "https://facebookresearch.github.io/SustainableConcrete/",
42+
"description": "Bayesian Optimization model for predicting concrete compressive strength, global warming potential, and cost from mix composition. Runs entirely in the browser.",
43+
"applicationCategory": "EngineeringApplication",
44+
"operatingSystem": "Any",
45+
"browserRequirements": "Requires JavaScript and a modern browser",
46+
"offers": {
47+
"@type": "Offer",
48+
"price": "0",
49+
"priceCurrency": "USD"
50+
},
51+
"author": [
52+
{"@type": "Person", "name": "Bayezid Baten"},
53+
{"@type": "Person", "name": "M. Ayyan Iqbal"},
54+
{"@type": "Person", "name": "Sebastian Ament"},
55+
{"@type": "Person", "name": "Julius Kusuma"},
56+
{"@type": "Person", "name": "Nishant Garg"}
57+
],
58+
"citation": [
59+
{
60+
"@type": "ScholarlyArticle",
61+
"name": "BOxCrete: A Bayesian Optimization Open-Source AI Model for Concrete Strength Forecasting and Mix Optimization",
62+
"url": "https://arxiv.org/abs/2603.21525"
63+
},
64+
{
65+
"@type": "ScholarlyArticle",
66+
"name": "Sustainable Concrete via Bayesian Optimization",
67+
"url": "https://arxiv.org/abs/2310.18288"
68+
}
69+
]
70+
}
71+
</script>
72+
873
<link rel="icon" type="image/svg+xml" href="favicon.svg">
974
<link rel="stylesheet" href="style.css">
1075
<link rel="preconnect" href="https://fonts.googleapis.com">
@@ -154,7 +219,7 @@ <h2>Predicted Strength Curve</h2>
154219
<div class="readouts" id="readouts">
155220
<div>GWP: <span class="value" id="gwp-value"></span> <span class="unit" id="gwp-unit">kg CO₂/m³</span></div>
156221
<div>Cost: <span class="value" id="cost-value"></span> <span id="cost-uncertainty"></span> <span class="unit" id="cost-unit">$/m³</span></div>
157-
<div>W/B: <span class="value" id="wb-value"></span></div>
222+
<div class="wb-readout">W/B: <span class="value" id="wb-value"></span></div>
158223
<!-- TODO: Add Slump readout once model/slump.json is available. -->
159224
</div>
160225
</div>

docs/og-image.jpg

147 KB
Loading

docs/robots.txt

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
# BOxCrete interactive explorer — crawler hints.
2+
# All assets under docs/ are public; no auth or rate-limited surfaces here.
3+
# Sitemap path is informational: GitHub Pages doesn't auto-discover robots.txt
4+
# at project subpaths, so this file is mainly for Lighthouse and direct fetches.
5+
User-agent: *
6+
Allow: /
7+
Sitemap: https://facebookresearch.github.io/SustainableConcrete/sitemap.xml

docs/sitemap.xml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
3+
<url>
4+
<loc>https://facebookresearch.github.io/SustainableConcrete/</loc>
5+
<lastmod>2026-05-07</lastmod>
6+
<changefreq>weekly</changefreq>
7+
<priority>1.0</priority>
8+
</url>
9+
</urlset>

docs/style.css

Lines changed: 205 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -427,23 +427,70 @@ h2 {
427427
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08), inset 0 0 0 1px var(--border);
428428
}
429429

430-
/* Sliders panel */
431-
.sliders { grid-column: 1; overflow-y: auto; max-height: 80vh; position: relative; }
430+
/* Sliders panel
431+
Note: no `max-height` / `overflow-y` here — the panel grows to fit all
432+
ingredient rows naturally on desktop. The references panel in column 3 is
433+
typically taller, so the composition panel still ends within the page flow. */
434+
.sliders { grid-column: 1; position: relative; }
432435
.sliders-header {
433436
display: flex;
434437
align-items: center;
435438
justify-content: space-between;
436439
margin-bottom: 0.25rem;
437440
}
438441
.sliders-header h2 { margin: 0; }
439-
.slider-group { margin-bottom: 0.75rem; }
442+
.slider-group { margin-bottom: 0.55rem; }
440443
.slider-group label {
441444
display: flex;
445+
/* Vertically align name text with the value-input's text. Without
446+
`align-items: center` the default `stretch` makes the name span and
447+
the value-input fill the row's full height, but the name text sits
448+
at the top of the line box while the value-input's text sits in its
449+
own vertical middle — visually offset. Centering the children's
450+
content axes pins them to the same baseline. */
451+
align-items: center;
442452
justify-content: space-between;
443453
font-size: 0.8rem;
444454
margin-bottom: 2px;
445455
}
446456
.slider-group label span { font-weight: 600; }
457+
458+
/* Click-to-edit composition value (regular sliders only — not Material Source).
459+
Visual treatment mirrors `.toggle-btn`: rectilinear with 4px radius and
460+
accent-colored focus border, matching the existing glass-morph language.
461+
Width is 6.5ch to accommodate the worst-case formatted value across both
462+
unit systems — e.g. water 700 kg/m³ in lb/yd³ = `1180.6` (six chars). */
463+
.slider-value {
464+
font: inherit;
465+
font-weight: 600;
466+
text-align: end;
467+
width: 6.5ch;
468+
min-width: 6.5ch;
469+
padding: 2px 4px;
470+
border: 1px solid transparent;
471+
border-radius: 4px;
472+
background: transparent;
473+
color: inherit;
474+
cursor: text;
475+
transition: background 0.15s, border-color 0.15s;
476+
/* Hide native number-input spinners (defensive — input type is `text` here
477+
but `inputmode=decimal` may map to spinner controls in some engines). */
478+
appearance: textfield;
479+
-moz-appearance: textfield;
480+
}
481+
.slider-value::-webkit-inner-spin-button,
482+
.slider-value::-webkit-outer-spin-button {
483+
-webkit-appearance: none;
484+
margin: 0;
485+
}
486+
.slider-value:hover {
487+
background: rgba(var(--card-bg-rgb), 0.4);
488+
}
489+
.slider-value:focus {
490+
outline: none;
491+
border-color: var(--accent);
492+
background: rgba(var(--card-bg-rgb), 0.6);
493+
}
447494
.slider-group input[type=range] {
448495
width: 100%;
449496
cursor: pointer;
@@ -481,21 +528,35 @@ h2 {
481528
/* Slider preview marker (shown on scatter hover) — hollow circle like a ghost thumb */
482529
.slider-group {
483530
position: relative;
484-
}
485-
/* Clickable ingredient names */
531+
/* Half the rendered thumb width, exposed as a CSS variable so the JS
532+
marker-positioning math (`showSliderPreview` in ui.mjs) can derive
533+
the same value via `getComputedStyle(...).getPropertyValue('--thumb-half')`.
534+
Mobile overrides this to 8 px because the thumb is shrunk to 16 px
535+
in the @media block below; without that override, the preview marker
536+
sat 1 px off-track on mobile. */
537+
--thumb-half: 9px;
538+
}
539+
/* Clickable ingredient names.
540+
Use `text-decoration` instead of `border-bottom` for the dashed
541+
underline so the line sits at the font's natural baseline-offset
542+
(just under the glyphs) rather than below the descender — saves a
543+
few pixels of vertical space per row, especially noticeable on
544+
mobile where every ingredient row is stacked. */
486545
.ingredient-name {
487546
cursor: pointer;
488-
border-bottom: 1px dashed var(--text-secondary);
489-
transition: color 0.15s, border-color 0.15s;
547+
text-decoration: underline dashed var(--text-secondary);
548+
text-decoration-thickness: 1px;
549+
text-underline-offset: 4px;
550+
transition: color 0.15s, text-decoration-color 0.15s;
490551
}
491552
.ingredient-name:hover {
492553
color: var(--accent);
493-
border-color: var(--accent);
554+
text-decoration-color: var(--accent);
494555
}
495556
.ingredient-name.active {
496557
color: var(--accent);
497-
border-bottom-style: solid;
498-
border-color: var(--accent);
558+
text-decoration-style: solid;
559+
text-decoration-color: var(--accent);
499560
}
500561

501562
/* Ingredient insight panel */
@@ -735,6 +796,17 @@ h2 {
735796
background: rgba(var(--card-bg-rgb), 0.5);
736797
color: var(--text);
737798
transition: border-color 0.15s;
799+
/* Hide native number spinner buttons. Chromium shows them by default,
800+
which steals ~14 px on the right and clips the "min"/"max"
801+
placeholders to "mi"/"ma". Mobile browsers don't render spinners,
802+
which is why the bug only manifested on desktop. */
803+
appearance: textfield;
804+
-moz-appearance: textfield;
805+
}
806+
.filter-row input[type=number]::-webkit-inner-spin-button,
807+
.filter-row input[type=number]::-webkit-outer-spin-button {
808+
-webkit-appearance: none;
809+
margin: 0;
738810
}
739811
.filter-row input[type=number]:focus {
740812
border-color: var(--accent);
@@ -865,7 +937,7 @@ h2 {
865937
margin: 0.1rem 0;
866938
}
867939
.ref-desc {
868-
font-size: 0.75rem;
940+
font-size: 0.82rem;
869941
color: var(--text);
870942
line-height: 1.4;
871943
margin: 0.2rem 0 0;
@@ -1224,11 +1296,129 @@ a.ref-link {
12241296
font-size: 0.78rem;
12251297
}
12261298

1227-
/* Sliders content inside unified panel */
1228-
.mobile-sliders-view .slider-group { margin-bottom: 0.4rem; }
1229-
.mobile-sliders-view .slider-group label { font-size: 0.72rem; }
1299+
/* Sliders content inside unified panel.
1300+
Mobile uses a vertical multi-row layout:
1301+
row 1: <label> with name (LEFT) and value-input (RIGHT) — full panel
1302+
content width via the base flex-between rule.
1303+
row 2: <input type=range> CENTERED, narrower than the panel so the
1304+
row 1 / row 3 edge labels (name↔min on the left, value↔max on
1305+
the right) stack vertically without the slider getting in
1306+
their way.
1307+
row 3: <div class="info-row"> with min (LEFT) and max (RIGHT) —
1308+
also full panel content width.
1309+
Vertical spacing is tightened so the three rows feel like a single
1310+
compact block (small label margin-bottom, small slider-group
1311+
margin-bottom). */
1312+
.mobile-sliders-view .slider-group {
1313+
margin-bottom: 0.3rem;
1314+
/* `.slider-group` is `position: relative` for the slider preview marker
1315+
(set globally above). */
1316+
}
1317+
.mobile-sliders-view .slider-group label {
1318+
/* Inherits `display: flex; justify-content: space-between` from the base
1319+
rule — name on the left, value-input on the right. 0.85rem matches
1320+
the mobile h2 size and bumps legibility on touch devices; both the
1321+
name span and the value-input inherit this size (the input uses
1322+
`font: inherit`). The mobile-value-fit specs assert the resulting
1323+
6.5ch input still fits within the panel at 320 px. */
1324+
font-size: 0.85rem;
1325+
margin-bottom: 0;
1326+
}
1327+
.mobile-sliders-view .slider-group .slider-value {
1328+
/* Tap-target tuning: ensure ≥ 32px height for thumbs while keeping
1329+
the rectilinear 4px radius. */
1330+
min-height: 32px;
1331+
padding: 4px 6px;
1332+
}
1333+
.mobile-sliders-view .slider-group input[type=range] {
1334+
/* Constrain the slider track to a centered, fixed-but-responsive width
1335+
so it sits in the middle of the panel rather than spanning the full
1336+
width. `min(60vw, 220px)` is comfortable on phones from 320 px up:
1337+
320 px viewport → 60vw = 192 px (capped to 192 px)
1338+
412 px viewport → 60vw = 247 px (capped to 220 px)
1339+
`display: block; margin: 4px auto;` centers horizontally and adds a
1340+
small breathing margin from the label above and info-row below. */
1341+
width: min(60vw, 220px);
1342+
display: block;
1343+
margin: 4px auto;
1344+
}
1345+
/* Smaller thumb on mobile reduces the intrinsic thumb-half-width inset
1346+
(was 9 px for the 18 px desktop thumb). 16 px is still a comfortable
1347+
touch target combined with the row's vertical generosity. The CSS
1348+
variable below keeps the JS slider-preview-marker math in sync —
1349+
`showSliderPreview` reads `--thumb-half` from the slider-group. */
1350+
.mobile-sliders-view .slider-group {
1351+
--thumb-half: 8px;
1352+
}
1353+
.mobile-sliders-view .slider-group input[type=range]::-webkit-slider-thumb {
1354+
width: 16px;
1355+
height: 16px;
1356+
}
1357+
.mobile-sliders-view .slider-group input[type=range]::-moz-range-thumb {
1358+
width: 16px;
1359+
height: 16px;
1360+
}
1361+
.mobile-sliders-view .slider-group .info-row {
1362+
/* Right-align the max-bound text to the same pixel column as the
1363+
`.slider-value` input's rendered glyph end. The value input has
1364+
1 px border + 6 px right padding = 7 px inset; matching that here
1365+
lines up the displayed value with the printed maximum exactly.
1366+
Left padding is zeroed so the minimum bound aligns with the
1367+
left-aligned ingredient name above. */
1368+
padding: 0 7px 0 0;
1369+
}
1370+
/* Material Source row on mobile: hide the redundant value-span (the active
1371+
toggle button is self-labeling). The toggle row sits below the label as
1372+
a normal block-flow child, just like an `input[type=range]` would. */
1373+
.mobile-sliders-view .material-source-group label > span:last-child {
1374+
display: none;
1375+
}
12301376

1231-
h2 { font-size: 0.85rem; }
1377+
/* --- Mobile legibility tuning ---
1378+
Touch-device read distances and older-eye comfort favour ~16 px (= 1rem)
1379+
for body copy. Bump the panel-body text and section headers uniformly
1380+
so the hierarchy stays consistent. We deliberately leave layout-tight
1381+
components (`.controls`, `.filter-row`, `.site-header h1`) at their
1382+
existing sizes to avoid wrapping or clipping in chrome that's already
1383+
space-constrained on small screens. */
1384+
h2 { font-size: 0.95rem; }
1385+
.mix-insight-text,
1386+
.ingredient-insight-text,
1387+
.ref-desc {
1388+
/* Bumped from 0.82–0.85rem. Keeps the three panels typographically
1389+
unified — the `font-uniformity.spec.ts` invariant still holds. */
1390+
font-size: 0.95rem;
1391+
line-height: 1.5;
1392+
}
1393+
.mix-insight-title,
1394+
.ingredient-insight-title {
1395+
/* Uppercase pill labels: bump enough to be comfortably readable but
1396+
still smaller than the body so the hierarchy reads "header → body". */
1397+
font-size: 0.85rem;
1398+
}
1399+
.references-panel h3 {
1400+
font-size: 0.95rem;
1401+
}
1402+
.ref-authors {
1403+
/* Author byline directly under the ref title. Was 0.75rem; bump
1404+
slightly to keep up with the 0.95rem ref-desc above. */
1405+
font-size: 0.85rem;
1406+
}
1407+
.readouts {
1408+
/* Override the earlier `font-size: 0.78rem` block above with a more
1409+
legible 0.88rem for the GWP / Cost / W/B readout strip. */
1410+
font-size: 0.88rem;
1411+
}
1412+
/* Hide the W/B (water-to-binder ratio) readout on mobile. With the bumped
1413+
0.88rem font, GWP + Cost already fill the strip; adding W/B forces a
1414+
wrap that steals vertical space from the strength curve canvas above.
1415+
Power users on mobile still have the slider readouts to infer water/
1416+
binder; W/B remains visible on desktop where there's room. */
1417+
.wb-readout { display: none; }
1418+
.mobile-hint {
1419+
/* Italic instructional hint inside the unified mobile panel header. */
1420+
font-size: 0.8rem;
1421+
}
12321422
}
12331423

12341424
/* Landscape mobile — panels side by side */

0 commit comments

Comments
 (0)