MS Calculator
Custom Milanscore calculator plugin
Install
This is the repository archive. It installs, but the folder name will carry the branch suffix and updates will not flow.
Install straight from GitHub with WP-CLI:
wp plugin install https://github.com/finoz/wpp-ms-calculator/archive/refs/heads/master.zipLightweight WordPress plugin that renders a configurable scoring calculator: a handful of input parameters (age, gender, ...), a score out.
Zero external dependencies. Scoring happens entirely client-side (vanilla JS, no REST calls): the field → points mapping and the result bands travel down embedded in the page as JSON, next to the form. PHP 8.0+, WP 6.0+.
Structure and update mechanism (GitHub auto-update, JSON config editor in admin) mirror the sibling plugin fnz-forms.
Quick start
- Upload
ms-calculator/towp-content/plugins/and activate. - Go to Settings → MS Calculator and paste your JSON configuration.
- Add the shortcode to any page:
[ms_calculator id="milan_score"]
To iterate on the interaction without WordPress: open preview/milan-score.html directly in a browser — it statically replicates the markup msc_render_calculator() generates from the bundled config-default.json.
Shortcode
[ms_calculator id="your_calculator"]
id must match a key in the calculators object of your config.
Configuration (JSON)
The full config lives in Settings → MS Calculator → Calculator configuration (stored in the database). Alternatively you can place a file at wp-content/ms-calculator-config.json — the admin UI always takes priority.
Minimal example
{
"calculators": {
"milan_score": {
"title": "Milan Score 2.0",
"fields": [
{
"id": "age",
"type": "radio",
"label": "Age",
"required": true,
"options": [
{ "value": "age_40", "label": "≤ 40 years", "points": 0 },
{ "value": "age_70", "label": "> 70 years", "points": 2 }
]
},
{
"id": "gender",
"type": "radio",
"label": "Gender",
"required": true,
"options": [
{ "value": "gender_f", "label": "Female", "points": 0 },
{ "value": "gender_m", "label": "Male", "points": 3 }
]
}
],
"percentages": { "0": 8, "1": 21, "2": 100 },
"bands": [
{ "min": 0, "max": 1, "label": "Unlikely" },
{ "min": 2, "max": 2, "label": "Very likely" }
]
}
}
}
The score updates live as fields are filled in — there is no "Calculate" button: the intended interaction is the clinician clicking through the patient's values in sequence and reading the result once every required field has been answered (see assets/ms-calculator.js). Once complete, the result panel shows three independent readings of the same total: Total score, % of risk, and Risk class.
Calculator-level keys
| Key | Required | Default | Description |
|---|---|---|---|
title |
no | — | Title shown above the form |
intro |
no | — | Intro copy |
fields |
yes | — | Array of field objects |
percentages |
no | — | Point-exact score → percentage lookup, feeds the "% of risk" row (see below) |
bands |
no | [] |
Min/max ranges with a verdict label each, feed the "Risk class" row (see below) |
Field object
| Key | Required | Default | Description |
|---|---|---|---|
id |
yes | — | Unique within the calculator; becomes the input name |
type |
no | select |
See field types below |
label |
no | same as id |
Visible label text |
required |
no | false |
HTML required attribute (not on checkbox-group) |
options |
no | — | Required for select, radio, checkbox-group |
points |
no | 0 |
boolean only: points awarded when checked |
Field types
| Type | HTML element | Scoring |
|---|---|---|
select |
<select> |
Points of the chosen option |
radio |
group of <input type="radio"> |
Points of the chosen option |
checkbox-group |
several <input type="checkbox"> |
Sum of points of every checked option |
boolean |
single <input type="checkbox"> |
The field's points, if checked |
select exists but is discouraged when there are only a few options (≤5–6): with radio the person filling in the form sees every choice at a glance and picks it in one click, instead of the two clicks (open the menu, then choose) a native <select> requires — that's why the Milan Score 2.0 in config-default.json uses radio for all 7 fields, gender included.
options format (for select, radio, checkbox-group):
"options": [
{ "value": "age_40", "label": "≤ 40 years", "points": 0 },
{ "value": "age_70", "label": "> 70 years", "points": 2 }
]
value convention: {field_id}_{number}, where {number} is the clinical threshold with no separators (e.g. age_4070 for the 40–70 range). It only needs to stay unique within the field. For a field with just two options sharing the same cutoff (e.g. BMI < 25 / ≥ 25), a leading zero marks "below": bmi_025 vs bmi_25. For a field with no natural clinical number (e.g. gender), fall back to the option's initial: gender_f / gender_m.
The total score is the sum of the points of every field. percentages and bands are two independent readings of that same total — one numeric, one qualitative — shown side by side, not a fallback of one another.
Score → percentage lookup (percentages)
The total score is the sum of the points of every field. When each individual score has its own empirical value — as with the Milan Score 2.0, where the score/risk relationship is a lookup table, not a formula — use percentages: a plain number keyed by score (as a string). Feeds the % of risk row.
"percentages": { "0": 8, "1": 9, "20": 100 }
Displayed as {value}%, e.g. 9%.
Risk class (bands)
bands gives the total score a qualitative verdict: the first band whose [min, max] range contains the total is shown in the Risk class row, as {label} ({min}–{max} points) (both bounds inclusive and optional — omitting min means -∞, omitting max means +∞; the range in parentheses is computed from min/max, not written out by hand, so it can't drift out of sync with the label).
"bands": [
{ "min": 0, "max": 3, "label": "Unlikely" },
{ "min": 4, "max": 10, "label": "Likely" },
{ "min": 11, "max": 20, "label": "Very likely" }
]
→ e.g. score 7 shows Risk class: Likely (4–10 points).
Generated markup
No "real" CSS is shipped by the plugin — assets/ms-calculator.css is a text placeholder pending the final design (see next section). Field markup reuses the same classes as fnz-forms (.form-group, label after/inside the input) so both plugins automatically pick up the same theme-level styling. Full example (2 fields out of 7) in preview/milan-score.html:
<div class="msc-calculator-wrap">
<h3 class="msc-calculator__title">Milan Score 2.0</h3>
<form class="form msc-calculator" id="milan_score_calculator" data-msc-id="milan_score" novalidate>
<div class="form-group form-group--radio" role="group" aria-label="Gender">
<span class="form-group__legend">Gender</span>
<div class="form-group__options">
<label for="milan_score_gender_0">
<input type="radio" id="milan_score_gender_0" name="milan_score_gender" value="gender_f" required>
<span>Female</span>
</label>
<label for="milan_score_gender_1">
<input type="radio" id="milan_score_gender_1" name="milan_score_gender" value="gender_m" required>
<span>Male</span>
</label>
</div>
</div>
<!-- … the other 6 questions, same pattern … -->
</form>
<!-- Result: always visible, updated live on every click -->
<div class="msc-result" id="milan_score_result" data-msc-result aria-live="polite">
<p class="msc-result__row"><span class="msc-result__caption">Total score</span><span class="msc-result__value" data-msc-result-score>0</span></p>
<p class="msc-result__row"><span class="msc-result__caption">% of risk</span><span class="msc-result__value" data-msc-result-percent>—</span></p>
<p class="msc-result__row"><span class="msc-result__caption">Risk class</span><span class="msc-result__value" data-msc-result-class>—</span></p>
<p class="msc-result__hint" data-msc-result-hint>Fill in all fields to see the risk.</p>
</div>
<script type="application/json" id="milan_score_msc-config">{ "fields": […], "percentages": {…}, "bands": […] }</script>
</div>
How scoring works
No server call, no "Calculate" button: assets/ms-calculator.js listens for change events on the form, recalculates the score on every click by reading the `