Developing Size Selection by Measurements for 1C-Bitrix
A size converter helps those who already know their size in one system. Size selection by measurements solves a different problem: the buyer knows their measurements — height, weight, chest circumference, foot length — but does not know which size will fit them in a specific brand. Or they know that with different manufacturers they wear different sizes, and want a recommendation specifically for the item in question.
This is the "smartest" of all size navigation tools — and the most complex to implement.
The Challenge
Size selection by measurements requires linking three sets of data:
- Buyer's measurements — height, weight, chest/waist/hip circumference, foot length
- Product size grid — the correspondence between size labels and physical measurements (accounting for fit allowances: slim or relaxed cut)
- Stock levels — the recommended size may be out of stock; an alternative must be suggested
And while for clothing the selection is relatively straightforward (chest → size), for footwear the question of width enters the picture (a wide foot requires a different last), and for sporting goods — technical specifications (stick length based on height and playing style).
Data Architecture
We extend the size chart structure with additional fit tolerance parameters:
CREATE TABLE b_size_fit_rules (
ID SERIAL PRIMARY KEY,
CHART_ID INT NOT NULL REFERENCES b_size_charts(ID),
SIZE_RU VARCHAR(10),
-- Measurement ranges for this size
CHEST_MIN NUMERIC(5,1), CHEST_MAX NUMERIC(5,1),
WAIST_MIN NUMERIC(5,1), WAIST_MAX NUMERIC(5,1),
HIPS_MIN NUMERIC(5,1), HIPS_MAX NUMERIC(5,1),
HEIGHT_MIN SMALLINT, HEIGHT_MAX SMALLINT,
WEIGHT_MIN SMALLINT, WEIGHT_MAX SMALLINT,
-- Fit type (affects tolerance)
FIT_TYPE VARCHAR(20) -- 'slim', 'regular', 'oversized'
);
The key point is measurement ranges, not exact values. Size 46 fits a chest of 88–92 cm, not strictly 90 cm. The width of the range depends on the fit type: slim fit — narrow range, oversized — wide range.
Selection Algorithm
// SizeFitAdvisor.php
class SizeFitAdvisor {
public function recommend(array $measurements, int $chartId, string $fitType = 'regular'): array {
$rules = SizeFitRulesTable::getList([
'filter' => ['=CHART_ID' => $chartId, '=FIT_TYPE' => $fitType],
'order' => ['SIZE_RU' => 'ASC'],
])->fetchAll();
$scores = [];
foreach ($rules as $rule) {
$score = 0;
$matched = 0;
// For each provided measurement, check whether it falls within the range
foreach (['CHEST', 'WAIST', 'HIPS'] as $param) {
if (!isset($measurements[$param])) continue;
$val = (float) $measurements[$param];
$min = (float) $rule[$param . '_MIN'];
$max = (float) $rule[$param . '_MAX'];
if ($val >= $min && $val <= $max) {
$score++;
} elseif ($val < $min) {
$score -= ($min - $val) / 10; // penalty for deviation
} else {
$score -= ($val - $max) / 10;
}
$matched++;
}
if ($matched > 0) {
$scores[$rule['SIZE_RU']] = $score / $matched;
}
}
arsort($scores); // sort by descending score
$best = array_key_first($scores);
$next = array_keys($scores)[1] ?? null; // second option in case of unavailability
return ['primary' => $best, 'alternative' => $next, 'scores' => $scores];
}
}
The algorithm returns not one size, but a primary recommendation and an alternative. This is important: if the primary size is unavailable, we show the alternative with a note such as "if 46 is out of stock, try 48 — it will work for your build."
Checking Recommended Size Availability
After obtaining the recommendation — immediately check stock levels:
public function checkAvailability(string $sizeRu, int $productId): array {
// Find trade offers with this size
$offers = \Bitrix\Iblock\ElementTable::getList([
'filter' => [
'=IBLOCK_ID' => OFFERS_IBLOCK_ID,
'=PROPERTY_CML2_LINK' => $productId,
'=PROPERTY_SIZE_VALUE' => $sizeRu,
'=ACTIVE' => 'Y',
],
'select' => ['ID', 'PROPERTY_QUANTITY'],
])->fetchAll();
$available = array_filter($offers, fn($o) => (int)$o['PROPERTY_QUANTITY_VALUE'] > 0);
return [
'available' => count($available) > 0,
'offers' => $available,
];
}
Selection Form on the Product Card
UX pattern: a "Find my size" link below the size selector opens a modal window or expands a form.
Single-step form (all measurements at once) — for experienced shoppers:
<form class="size-advisor-form">
<div class="form-row">
<label>Height (cm): <input type="number" name="height" min="140" max="220"></label>
<label>Weight (kg): <input type="number" name="weight" min="40" max="200"></label>
</div>
<div class="form-row">
<label>Chest circumference (cm): <input type="number" name="chest" min="60" max="160"></label>
<label>Waist circumference (cm): <input type="number" name="waist" min="50" max="150"></label>
<label>Hip circumference (cm): <input type="number" name="hips" min="70" max="170"></label>
</div>
<label>Fit:
<select name="fit_type">
<option value="slim">Slim</option>
<option value="regular" selected>Regular</option>
<option value="oversized">Oversized</option>
</select>
</label>
<button type="submit">Find my size</button>
</form>
Step-by-step questionnaire — for users unfamiliar with such forms. One field per screen, with a progress bar. Less cognitive load, but a longer process.
AJAX Request to the PHP Controller
The form sends data to the server rather than being processed in JS, because:
- The selection algorithm is server-side — data is not exposed to competitors
- The server checks stock immediately and returns a final response
- The result can be personalised (measurements saved for an authenticated user)
// SizeAdvisorController.php — AJAX action
public function recommendAction(): array {
$measurements = [
'CHEST' => (float) $this->request->getPost('chest'),
'WAIST' => (float) $this->request->getPost('waist'),
'HIPS' => (float) $this->request->getPost('hips'),
];
$fitType = $this->request->getPost('fit_type', 'regular');
$productId = (int) $this->request->getPost('product_id');
$chartId = $this->getChartForProduct($productId);
$advisor = new SizeFitAdvisor();
$result = $advisor->recommend($measurements, $chartId, $fitType);
$availability = $advisor->checkAvailability($result['primary'], $productId);
// Save measurements for the authenticated user
if (is_object(global_user()) && !global_user()->IsGuest()) {
UserMeasurementsTable::saveForUser(global_user()->GetID(), $measurements);
}
return [
'recommended_size' => $result['primary'],
'alternative_size' => $result['alternative'],
'available' => $availability['available'],
'offers' => $availability['offers'],
];
}
Saving Measurements in the User Account
If the user is authenticated, measurements are saved and pre-filled the next time the advisor is used — even on a different product:
CREATE TABLE b_user_measurements (
USER_ID INT PRIMARY KEY REFERENCES b_user(ID),
HEIGHT_CM SMALLINT,
WEIGHT_KG SMALLINT,
CHEST_CM NUMERIC(5,1),
WAIST_CM NUMERIC(5,1),
HIPS_CM NUMERIC(5,1),
FOOT_MM INT,
DATE_UPDATE TIMESTAMP DEFAULT NOW()
);
In the user account — a "My Measurements" section for manual editing.
Timelines
| Option | What's included | Timeline |
|---|---|---|
| Basic advisor (JS + data table) | Form, algorithm, result | 1–2 weeks |
| With stock check and alternatives | + TO integration, stock levels | 2–3 weeks |
| + Account with measurements, recommendation history | + profile saving | +1 week |
A measurement-based size advisor provides the most accurate recommendation and maximally reduces the probability of returns. Of all the size tools, it is the most labour-intensive — but also the most effective.







