PageSourceSearch

https://percentilegrowthcharts.com/assets/js/calculator.js

js percentilegrowthcharts.com collected 2026-09-24 14:49:51 UTC 14,318 bytes, 447 lines download raw bytes

1(function () {
2  function qs(sel, root) {
3    return (root || document).querySelector(sel);
4  }
5  function qsa(sel, root) {
6    return Array.prototype.slice.call((root || document).querySelectorAll(sel));
7  }
8
9  var INCH = 2.54;
10  var LB = 0.45359237;
11  var i18n = window.PGC_I18N || {};
12  var form = qs('#pgc-form');
13  if (!form) {
14    return;
15  }
16  if (window.Chart && window.ChartZoom) {
17    Chart.register(ChartZoom);
18  }
19  if (window.Chart) {
20    Chart.defaults.font.family = '"Inter", "Open Sans", sans-serif';
21    Chart.defaults.font.size = 12;
22    Chart.defaults.color = '#5d6b7e';
23  }
24
25  var charts = {};
26  var units = 'metric';
27  var dSel = qs('#dob-d');
28  var mSel = qs('#dob-m');
29  var ySel = qs('#dob-y');
30  var months = i18n.months || ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'];
31  var nowY = new Date().getFullYear();
32  var i;
33
34  function addOption(select, value, label) {
35    var o = document.createElement('option');
36    o.value = String(value);
37    o.textContent = label;
38    select.appendChild(o);
39  }
40
41  for (i = 1; i <= 31; i++) {
42    addOption(dSel, i, i);
43  }
44  for (i = 1; i <= 12; i++) {
45    addOption(mSel, i, months[i - 1]);
46  }
47  for (i = nowY; i >= nowY - 21; i--) {
48    addOption(ySel, i, i);
49  }
50
51  function defaultDob() {
52    dSel.value = '1';
53    mSel.value = '8';
54    ySel.value = String(nowY - 10);
55  }
56
57  function setText(sel, text) {
58    qsa(sel).forEach(function (el) {
59      el.textContent = text;
60    });
61  }
62
63  function setUnits(u, convert) {
64    var prev = units;
65    units = u;
66    qs('#btn-metric').classList.toggle('active', u === 'metric');
67    qs('#btn-imperial').classList.toggle('active', u === 'imperial');
68    form.classList.toggle('is-metric', u === 'metric');
69    form.classList.toggle('is-imperial', u === 'imperial');
70    var metric = u === 'metric';
71    setText('.u-h', metric ? i18n.cm : i18n.in);
72    setText('.u-w', metric ? i18n.kg : i18n.lb);
73    setText('.u-c', metric ? i18n.cm : i18n.in);
74    qs('#height-imp').hidden = metric;
75    qsa('.height-metric').forEach(function (el) {
76      el.style.display = metric ? '' : 'none';
77    });
78    if (convert && prev !== u) {
79      var h = parseFloat(qs('#height').value);
80      var w = parseFloat(qs('#weight').value);
81      var c = parseFloat(qs('#head').value);
82      if (u === 'imperial') {
83        if (!isNaN(h) && h > 0) {
84          var inches = h / INCH;
85          qs('#height-ft').value = String(Math.floor(inches / 12));
86          qs('#height-in').value = (inches % 12).toFixed(1);
87        }
88        if (!isNaN(w) && w > 0) {
89          qs('#weight').value = (w / LB).toFixed(2);
90        }
91        if (!isNaN(c) && c > 0) {
92          qs('#head').value = (c / INCH).toFixed(1);
93        }
94      } else {
95        var tot = (parseFloat(qs('#height-ft').value) || 0) * 12 + (parseFloat(qs('#height-in').value) || 0);
96        if (tot > 0) {
97          qs('#height').value = (tot * INCH).toFixed(1);
98        }
99        if (!isNaN(w) && w > 0) {
100          qs('#weight').value = (w * LB).toFixed(2);
101        }
102        if (!isNaN(c) && c > 0) {
103          qs('#head').value = (c * INCH).toFixed(1);
104        }
105      }
106      syncInches();
107    }
108    persist();
109  }
110
111  function syncInches() {
112    var tot = (parseFloat(qs('#height-ft').value) || 0) * 12 + (parseFloat(qs('#height-in').value) || 0);
113    qs('#total-in').textContent = tot ? tot.toFixed(1) : '0';
114  }
115
116  function persist() {
117    var sexEl = qs('input[name="sex"]:checked', form);
118    var data = {
119      units: units,
120      d: dSel.value,
121      m: mSel.value,
122      y: ySel.value,
123      sex: sexEl ? sexEl.value : '',
124      height: qs('#height').value,
125      heightFt: qs('#height-ft').value,
126      heightIn: qs('#height-in').value,
127      weight: qs('#weight').value,
128      head: qs('#head').value,
129      source: qs('#source').value
130    };
131    try { localStorage.setItem('pgc_form', JSON.stringify(data)); } catch (e) {}
132  }
133
134  function restore() {
135    try {
136      var raw = localStorage.getItem('pgc_form');
137      if (!raw) {
138        return;
139      }
140      var data = JSON.parse(raw);
141      units = data.units === 'imperial' ? 'imperial' : 'metric';
142      if (data.d) { dSel.value = String(data.d); }
143      if (data.m) { mSel.value = String(data.m); }
144      if (data.y) { ySel.value = String(data.y); }
145      if (data.sex) {
146        var r = qs('input[name="sex"][value="' + data.sex + '"]', form);
147        if (r) { r.checked = true; }
148      }
149      if (data.height) { qs('#height').value = data.height; }
150      if (data.heightFt) { qs('#height-ft').value = data.heightFt; }
151      if (data.heightIn) { qs('#height-in').value = data.heightIn; }
152      if (data.weight) { qs('#weight').value = data.weight; }
153      if (data.head) { qs('#head').value = data.head; }
154      if (data.source) {
155        var src = qs('#source');
156        var allowed = false;
157        var opts = src.options;
158        var oi;
159        for (oi = 0; oi < opts.length; oi++) {
160          if (opts[oi].value === data.source) {
161            allowed = true;
162            break;
163          }
164        }
165        src.value = allowed ? data.source : 'who';
166      }
167      syncInches();
168    } catch (e) {}
169  }
170
171  function ageMonths(y, m, d) {
172    var dob = new Date(parseInt(y, 10), parseInt(m, 10) - 1, parseInt(d, 10));
173    var now = new Date();
174    var monthsAge = (now.getFullYear() - dob.getFullYear()) * 12 + (now.getMonth() - dob.getMonth());
175    monthsAge += (now.getDate() - dob.getDate()) / 30.4375;
176    return monthsAge;
177  }
178
179  function interp(points, x) {
180    if (!points || !points.length) {
181      return null;
182    }
183    if (x < points[0][0] || x > points[points.length - 1][0]) {
184      if (x < points[0][0] && x >= points[0][0] - 0.6) {
185        return points[0][1];
186      }
187      if (x > points[points.length - 1][0] && x <= points[points.length - 1][0] + 0.6) {
188        return points[points.length - 1][1];
189      }
190      return null;
191    }
192    var n;
193    for (n = 0; n < points.length - 1; n++) {
194      var a = points[n];
195      var b = points[n + 1];
196      if (x >= a[0] && x <= b[0]) {
197        var t = (b[0] === a[0]) ? 0 : (x - a[0]) / (b[0] - a[0]);
198        return a[1] + t * (b[1] - a[1]);
199      }
200    }
201    return points[points.length - 1][1];
202  }
203
204  function percentileFromCurves(curves, x, y) {
205    var names = [3, 10, 25, 50, 75, 90, 97];
206    var ys = [];
207    var n;
208    for (n = 0; n < curves.length; n++) {
209      var v = interp(curves[n].data, x);
210      if (v === null) {
211        return { label: i18n.no_data, value: null };
212      }
213      ys.push(v);
214    }
215    if (y < ys[0]) {
216      return { label: i18n.below_p3, value: 3 };
217    }
218    if (y > ys[ys.length - 1]) {
219      return { label: i18n.above_p97, value: 97 };
220    }
221    for (n = 0; n < ys.length - 1; n++) {
222      if (y >= ys[n] && y <= ys[n + 1]) {
223        var t = (ys[n + 1] === ys[n]) ? 0 : (y - ys[n]) / (ys[n + 1] - ys[n]);
224        var p = names[n] + t * (names[n + 1] - names[n]);
225        return { label: 'P' + Math.round(p), value: p };
226      }
227    }
228    return { label: 'P50', value: 50 };
229  }
230
231  function toMetricHeight() {
232    if (units === 'metric') {
233      return parseFloat(qs('#height').value);
234    }
235    var tot = (parseFloat(qs('#height-ft').value) || 0) * 12 + (parseFloat(qs('#height-in').value) || 0);
236    return tot ? tot * INCH : NaN;
237  }
238  function toMetricWeight() {
239    var w = parseFloat(qs('#weight').value);
240    if (isNaN(w)) {
241      return NaN;
242    }
243    return units === 'metric' ? w : w * LB;
244  }
245  function toMetricHead() {
246    var c = parseFloat(qs('#head').value);
247    if (isNaN(c)) {
248      return NaN;
249    }
250    return units === 'metric' ? c : c * INCH;
251  }
252  function yFactor(kind) {
253    if (units === 'metric') {
254      return 1;
255    }
256    return kind === 'weight' ? 1 / LB : 1 / INCH;
257  }
258  function yUnit(kind) {
259    if (kind === 'weight') {
260      return units === 'metric' ? i18n.kg : i18n.lb;
261    }
262    return units === 'metric' ? i18n.cm : i18n.in;
263  }
264
265  function run() {
266    persist();
267    var err = qs('#form-error');
268    err.hidden = true;
269    var y = ySel.value;
270    var m = mSel.value;
271    var d = dSel.value;
272    var sexEl = qs('input[name="sex"]:checked', form);
273    var hCm = toMetricHeight();
274    var wKg = toMetricWeight();
275    var cCm = toMetricHead();
276    if (!y || !m || !d || !sexEl || (isNaN(hCm) && isNaN(wKg) && isNaN(cCm))) {
277      err.textContent = i18n.need_fields;
278      err.hidden = false;
279      return;
280    }
281    var age = ageMonths(y, m, d);
282    var headInput = qs('#head');
283    if (age > 36) {
284      headInput.disabled = true;
285      cCm = NaN;
286    } else {
287      headInput.disabled = false;
288    }
289    fetch('/data/percentile/' + qs('#source').value + '.json')
290      .then(function (r) { return r.json(); })
291      .then(function (json) {
292        var sex = sexEl.value;
293        qs('#results').hidden = false;
294        qs('#age-out').textContent = i18n.age_label + ': ' + age.toFixed(1) + ' ' + i18n.months_short;
295        showMetric('height', json.height && json.height[sex], age, hCm, 'out-h', i18n.pct_height, 'chart-h');
296        showMetric('weight', json.weight && json.weight[sex], age, wKg, 'out-w', i18n.pct_weight, 'chart-w');
297        showMetric('head', json.head && json.head[sex], age, cCm, 'out-c', i18n.pct_head, 'chart-c');
298      })
299      .catch(function () {
300        err.textContent = i18n.no_data;
301        err.hidden = false;
302      });
303  }
304
305  function renderStat(el, name, value) {
306    el.textContent = '';
307    var n = document.createElement('span');
308    n.className = 'pct-name';
309    n.textContent = name;
310    var v = document.createElement('span');
311    v.className = 'pct-value';
312    v.textContent = value;
313    el.appendChild(n);
314    el.appendChild(v);
315  }
316
317  function showMetric(kind, curves, age, val, outId, label, canvasId) {
318    var out = qs('#' + outId);
319    var block = qs('#block-' + (kind === 'height' ? 'h' : (kind === 'weight' ? 'w' : 'c')));
320    if (kind === 'head' && (isNaN(val) || !curves)) {
321      out.hidden = true;
322      if (block) {
323        block.hidden = true;
324      }
325      return;
326    }
327    out.hidden = false;
328    if (block) {
329      block.hidden = false;
330    }
331    if (isNaN(val) || !curves) {
332      renderStat(out, label, '—');
333      if (block) {
334        block.hidden = true;
335      }
336      return;
337    }
338    var pct = percentileFromCurves(curves, age, val);
339    renderStat(out, label, pct.label);
340    drawChart(canvasId, kind, curves, age, val);
341  }
342
343  function drawChart(id, kind, curves, age, val) {
344    var fac = yFactor(kind);
345    var colors = ['#b50033', '#e07090', '#7aa2c8', '#003b75', '#7aa2c8', '#e07090', '#b50033'];
346    var datasets = [];
347    var n;
348    for (n = 0; n < curves.length; n++) {
349      var mid = Math.floor(curves.length / 2);
350      datasets.push({
351        label: curves[n].name,
352        data: curves[n].data.map(function (p) { return { x: p[0], y: p[1] * fac }; }),
353        borderColor: colors[n % colors.length],
354        backgroundColor: 'transparent',
355        borderWidth: n === mid ? 2.4 : 1.4,
356        borderDash: n === mid ? [] : [5, 4],
357        pointRadius: 0,
358        tension: 0.3
359      });
360    }
361    datasets.push({
362      label: '•',
363      data: [{ x: age, y: val * fac }],
364      borderColor: '#fff',
365      backgroundColor: '#e70041',
366      borderWidth: 2.5,
367      pointRadius: 7,
368      pointHoverRadius: 9,
369      showLine: false
370    });
371    if (charts[id]) {
372      charts[id].destroy();
373    }
374    charts[id] = new Chart(qs('#' + id), {
375      type: 'line',
376      data: { datasets: datasets },
377      options: {
378        responsive: true,
379        maintainAspectRatio: false,
380        interaction: { mode: 'nearest', intersect: false },
381        plugins: {
382          legend: {
383            display: true,
384            position: 'bottom',
385            labels: { boxWidth: 10, boxHeight: 10, usePointStyle: true, pointStyle: 'line', padding: 14, color: '#5d6b7e' }
386          },
387          tooltip: {
388            backgroundColor: 'rgba(16, 26, 40, .92)',
389            padding: 10,
390            cornerRadius: 8,
391            titleFont: { weight: '700' },
392            displayColors: false,
393            callbacks: {
394              label: function (c) {
395                var yv = c.parsed.y;
396                return c.dataset.label + ': ' + (yv && yv.toFixed ? yv.toFixed(1) : yv) + ' ' + yUnit(kind);
397              }
398            }
399          },
400          zoom: { pan: { enabled: true, mode: 'xy' }, zoom: { wheel: { enabled: true }, pinch: { enabled: true }, mode: 'xy' } }
401        },
402        scales: {
403          x: {
404            type: 'linear',
405            title: { display: true, text: i18n.axis_age, color: '#5d6b7e' },
406            grid: { color: 'rgba(227, 233, 242, .8)' },
407            border: { color: '#e3e9f2' },
408            ticks: { color: '#5d6b7e' }
409          },
410          y: {
411            title: { display: true, text: yUnit(kind), color: '#5d6b7e' },
412            grid: { color: 'rgba(227, 233, 242, .8)' },
413            border: { color: '#e3e9f2' },
414            ticks: { color: '#5d6b7e' }
415          }
416        }
417      }
418    });
419  }
420
421  function resetZoom() {
422    Object.keys(charts).forEach(function (k) {
423      if (charts[k] && charts[k].resetZoom) {
424        charts[k].resetZoom();
425      }
426    });
427  }
428
429  qs('#btn-metric').addEventListener('click', function () { setUnits('metric', true); });
430  qs('#btn-imperial').addEventListener('click', function () { setUnits('imperial', true); });
431  qs('#height-ft').addEventListener('input', syncInches);
432  qs('#height-in').addEventListener('input', syncInches);
433  qs('#reset-zoom').addEventListener('click', resetZoom);
434  form.addEventListener('submit', function (e) {
435    e.preventDefault();
436    run();
437  });
438  form.addEventListener('change', persist);
439  form.addEventListener('input', persist);
440
441  defaultDob();
442  restore();
443  if (!dSel.value || !mSel.value || !ySel.value) {
444    defaultDob();
445  }
446  setUnits(units, false);
447})();

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.