viewof quarter = {
const n = quarters.length;
const initial = n - 1;
const THEME = "rgb(90, 75, 134)";
const THUMB = 9;
let timer = null;
// ── Current quarter display (centered, no border) with step arrows ──
const currentLabel = document.createElement("div");
currentLabel.textContent = "Current quarter";
Object.assign(currentLabel.style, {
fontSize: ".7rem",
color: "#666",
textTransform: "uppercase",
letterSpacing: ".08em"
});
const currentName = document.createElement("div");
currentName.textContent = quarters[initial];
Object.assign(currentName.style, {
fontSize: "1.25rem",
fontWeight: "600",
color: THEME,
lineHeight: "1.2"
});
const currentBox = document.createElement("div");
Object.assign(currentBox.style, { textAlign: "center", padding: "0 .75rem" });
currentBox.append(currentLabel, currentName);
const iconBtnStyle = {
background: "none",
border: "none",
color: THEME,
cursor: "pointer",
padding: ".25rem",
display: "flex",
alignItems: "center",
justifyContent: "center",
borderRadius: "4px",
lineHeight: "0"
};
const leftBtn = document.createElement("button");
leftBtn.setAttribute("aria-label", "Previous quarter");
leftBtn.innerHTML = '<svg width="22" height="22" viewBox="0 0 24 24" fill="none"><path d="M15 6 L9 12 L15 18" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>';
Object.assign(leftBtn.style, iconBtnStyle);
const rightBtn = document.createElement("button");
rightBtn.setAttribute("aria-label", "Next quarter");
rightBtn.innerHTML = '<svg width="22" height="22" viewBox="0 0 24 24" fill="none"><path d="M9 6 L15 12 L9 18" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>';
Object.assign(rightBtn.style, iconBtnStyle);
const headerRow = document.createElement("div");
Object.assign(headerRow.style, {
display: "flex",
alignItems: "center",
justifyContent: "center",
gap: ".25rem",
margin: ".25rem 0 .75rem 0"
});
headerRow.append(leftBtn, currentBox, rightBtn);
// ── Slider styling (scoped) ──
const cls = "tl" + Math.random().toString(36).slice(2, 8);
const styleEl = document.createElement("style");
styleEl.textContent = `
input.${cls} {
-webkit-appearance: none;
appearance: none;
width: 100%;
height: 4px;
background: transparent;
outline: none;
margin: 0;
padding: 0;
position: relative;
z-index: 2;
}
input.${cls}::-webkit-slider-runnable-track {
height: 4px;
background: #d6d3e0;
border-radius: 2px;
}
input.${cls}::-moz-range-track {
height: 4px;
background: #d6d3e0;
border-radius: 2px;
}
input.${cls}::-webkit-slider-thumb {
-webkit-appearance: none;
appearance: none;
width: 18px;
height: 18px;
border-radius: 50%;
background: ${THEME};
border: 3px solid white;
box-shadow: 0 0 0 2px ${THEME};
cursor: pointer;
margin-top: -7px;
}
input.${cls}::-moz-range-thumb {
width: 12px;
height: 12px;
border-radius: 50%;
background: ${THEME};
border: 3px solid white;
box-shadow: 0 0 0 2px ${THEME};
cursor: pointer;
}
`;
const slider = document.createElement("input");
slider.type = "range";
slider.classList.add(cls);
slider.min = 0;
slider.max = n - 1;
slider.step = 1;
slider.value = initial;
// Tick layer
const tickLayer = document.createElement("div");
Object.assign(tickLayer.style, {
position: "absolute",
top: "13px",
left: `${THUMB}px`,
right: `${THUMB}px`,
height: "10px",
pointerEvents: "none",
zIndex: "1"
});
for (let i = 0; i < n; i++) {
const pct = i / (n - 1);
const tick = document.createElement("div");
Object.assign(tick.style, {
position: "absolute",
left: `${pct * 100}%`,
transform: "translateX(-50%)",
width: "1px",
height: "8px",
background: "#9a91b3"
});
tickLayer.appendChild(tick);
}
const sliderWrap = document.createElement("div");
Object.assign(sliderWrap.style, { position: "relative", width: "100%", height: "26px" });
Object.assign(slider.style, { position: "absolute", top: "4px", left: "0", right: "0", width: "100%" });
sliderWrap.append(tickLayer, slider);
// Min / max quarter labels under the slider
const fyWrap = document.createElement("div");
Object.assign(fyWrap.style, { width: "100%", marginTop: ".25rem" });
const fyInner = document.createElement("div");
Object.assign(fyInner.style, {
marginLeft: `${THUMB}px`,
marginRight: `${THUMB}px`,
display: "flex",
justifyContent: "space-between",
fontSize: ".75rem",
color: "#666"
});
const minQ = document.createElement("span");
minQ.textContent = quarters[0];
const maxQ = document.createElement("span");
maxQ.textContent = quarters[n - 1];
fyInner.append(minQ, maxQ);
fyWrap.appendChild(fyInner);
// ── Play / pause icon button on the left of the slider ──
const PLAY_SVG = '<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor"><path d="M4 2 L13 8 L4 14 Z"/></svg>';
const PAUSE_SVG = '<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor"><rect x="3.5" y="2.5" width="3" height="11" rx="0.5"/><rect x="9.5" y="2.5" width="3" height="11" rx="0.5"/></svg>';
const playBtn = document.createElement("button");
playBtn.setAttribute("aria-label", "Play");
playBtn.innerHTML = PLAY_SVG;
Object.assign(playBtn.style, iconBtnStyle);
playBtn.style.flex = "0 0 auto";
// Slider column wraps (slider + FY brackets) so they stay aligned
const sliderCol = document.createElement("div");
Object.assign(sliderCol.style, { flex: "1 1 auto", minWidth: "0" });
sliderCol.append(sliderWrap, fyWrap);
const scrubberRow = document.createElement("div");
Object.assign(scrubberRow.style, {
display: "flex",
alignItems: "flex-start",
gap: ".5rem",
width: "100%"
});
scrubberRow.append(playBtn, sliderCol);
// ── Assemble ──
const container = document.createElement("div");
Object.assign(container.style, {
width: "100%",
margin: "1rem 0",
border: "1px solid #ddd",
borderRadius: "6px",
padding: "1rem"
});
container.append(styleEl, headerRow, scrubberRow);
const refreshUi = () => {
const i = +slider.value;
currentName.textContent = quarters[i];
leftBtn.disabled = i <= 0;
rightBtn.disabled = i >= n - 1;
leftBtn.style.opacity = leftBtn.disabled ? "0.3" : "1";
rightBtn.style.opacity = rightBtn.disabled ? "0.3" : "1";
leftBtn.style.cursor = leftBtn.disabled ? "default" : "pointer";
rightBtn.style.cursor = rightBtn.disabled ? "default" : "pointer";
};
const update = () => {
const i = +slider.value;
refreshUi();
container.value = quarters[i];
container.dispatchEvent(new CustomEvent("input", { bubbles: true }));
};
function stopAnim() {
if (timer) {
clearInterval(timer);
timer = null;
playBtn.innerHTML = PLAY_SVG;
playBtn.setAttribute("aria-label", "Play");
}
}
slider.addEventListener("input", () => { stopAnim(); update(); });
leftBtn.addEventListener("click", () => {
stopAnim();
const i = +slider.value;
if (i > 0) { slider.value = i - 1; update(); }
});
rightBtn.addEventListener("click", () => {
stopAnim();
const i = +slider.value;
if (i < n - 1) { slider.value = i + 1; update(); }
});
playBtn.addEventListener("click", () => {
if (timer) {
stopAnim();
return;
}
// If at the end, restart from the beginning before playing
if (+slider.value >= n - 1) {
slider.value = 0;
update();
}
playBtn.innerHTML = PAUSE_SVG;
playBtn.setAttribute("aria-label", "Pause");
timer = setInterval(() => {
const next = +slider.value + 1;
if (next >= n) { stopAnim(); return; }
slider.value = next;
update();
}, 800);
});
refreshUi();
container.value = quarters[initial];
return container;
}Percentile Rank Calculator for Residential Aged Care Star Ratings (Now With May 2026 Extract)
Aged care star ratings
In December 2022 the Department of Health and Aged Care released a star rating system which helps people be more informed when selecting a government-funded aged care home. The overall star rating given to each home is determined by a combination of residents’ experience, staffing, compliance and quality measures (indicators). Specifically, the four sub-category ratings are combined as a weighted sum, where residents’ experience contributes 33%, compliance 30%, staffing 22%, and quality measures 15%. There is also one override: if a home’s compliance rating is 2 stars or below, the overall rating is capped at the compliance rating, so a serious compliance issue can’t be offset by strength elsewhere. The result is then rounded to the nearest whole star to produce the published overall rating. The full details for how the star ratings are determined is in the Star Ratings Provider Manual (Australian Government, Department of Health and Aged Care 2022). In my calculator, I used the star ratings calculated according to the manual, but before the final rounding step, to compute a percentile rank among Australian aged care homes.
How to use the calculator
Input a home’s star rating for the four sub-categories and the calculator will compute the overall star rating prior to rounding, the percentile rank of that star rating, and the overall star rating after rounding.
The calculator defaults to the most recent extract, but you can step through earlier quarters using the arrow buttons either side of the quarter label, or by dragging the slider directly. Pressing the play button to the left of the slider animates through each quarter in turn, so you can watch how the distribution of star ratings across Australian aged care homes has shifted over time. The red line marking your selected rating stays put while the histogram updates underneath it.
References
Australian Government, Department of Health and Aged Care. 2022. Star Ratings Provider Manual. https://www.health.gov.au/resources/publications/star-ratings-provider-manual?language=en.