shared_data = transpose(df_to_js)
// guard function
// makes a submit and reset button to use after multiple inputs made, see https://observablehq.com/@mootari/inputs-submit
function guard(fn, options = {}) {
const {
submitLabel = "Submit",
resetLabel = "Reset",
required = false,
resubmit = true,
width = "fit-content",
// width = "min-content",
justify = "start",
valid = input => !input.querySelector(":invalid"),
} = options;
const onSubmit = () => {
value = input.value;
submit.disabled = !resubmit || invalid;
reset.disabled = true;
wrapper.dispatchEvent(new Event("input", { bubbles: true }));
};
const onReset = () => {
input.value = value;
submit.disabled = !resubmit || invalid;
reset.disabled = true;
};
const onInnerInputCapture = () => {
invalid = true;
submit.disabled = true;
};
const onInnerInput = e => {
e.stopPropagation();
submit.disabled = false;
reset.disabled = false;
};
const submit = htl.html`<button ${{disabled: !resubmit && !required, onclick: onSubmit}}>${submitLabel}`;
//not actually going to show/use reset button but keeping logic in case we want
const reset = htl.html`<button ${{disabled: true, onclick: onReset}}>${resetLabel}`;
// const footer = htl.html`<div><hr style="padding:0;margin:10px 0"><div style="display:flex;gap:1ch;justify-content:${justify}">${submit} ${reset}`;
// which buttons to show and space around them, center submit button
const footer = htl.html`<div><hr style="padding:0;margin:10px 0"><div style="display:flex;gap:1ch;justify-content:${justify}">${submit}`;
// format the input toolbar, remove the extra inside <div>s commented to not stack them, but flex wrapping not working
const template = inputs => htl.html`
<div style="display: flex; flex-wrap: wrap; gap: 12px; width: 100%;">
<div>
${(Array.isArray(inputs) ? inputs : Object.values(inputs)).map(
input => htl.html`
<div style="flex: 1 1 200px; flex-wrap: wrap; min-width: 0;">
<span style="display: block; width: 100%;">${input}</span>
</div>
`
)}
</div>
</div>
${footer}
`;
const input = fn({submit, reset, footer, template, onSubmit, onReset});
let invalid = !!input.querySelector(":invalid");
submit.disabled = !resubmit || invalid;
input.addEventListener("input", onInnerInputCapture, {capture: true});
input.addEventListener("input", onInnerInput);
let value = required ? undefined : input.value;
const wrapper = htl.html`<div style="width:${width}">${input}`;
//const wrapper = htl.html`<div>${input}`;
wrapper.addEventListener("submit", onSubmit);
return Object.defineProperty(wrapper, "value", {
get: () => value,
set: (v) => { input.value = v },
});
}
// save unique Counties so I can use these in select popup
uniqueCounties = new Set(shared_data.map(d => d.COUNTY_DESC));viewof inputSels = guard(
({ template }) => Inputs.form(
{
county: Inputs.text({
value: "NASSAU",
label: "County:",
// placeholder: "Enter County…",
datalist: uniqueCounties,
unique: true,
validate: v => shared_data.map(d => d.COUNTY_DESC).includes(v.value.toUpperCase()),
sort: true,
required: true
// width: 75
}),
grade: Inputs.select(new Map([ ["Grade 3", "ELA3"], ["Grade 4", "ELA4"], ["Grade 5", "ELA5"], ["Grade 6", "ELA6"], ["Grade 7", "ELA7"], ["Grade 8", "ELA8"], ["All Grades (3-8)", "ELA3_8"] ]), {
value: "ELA3",
label: "Grade:",
sort: true,
required: true
// width: 75
}),
cohort: Inputs.select(new Map([ ["IEP / no IEP", "SWD"], ["Econ.Disadvantaged / not", "Econ"] ]), {
value: "SWD", // ← was "SWD", which doesn't exist in the map
label: "Cohorts:",
required: true
// width: 75
})
},
{ template }
),
// Options. Override the label of the reset button, require inputs pass validation
{ resetLabel: "Reset", required: true }
);fig2 = {
const width = 1000;
const barHeight = 24;
const height = 150 + shared_dataFiltered2.length * barHeight;
// Build stacked segments for each row
const proficient = shared_dataFiltered2.map(d => ({
entity: d.ENTITY_NAME,
x1: 0,
x2: d.Prof_Pct,
category: "Proficient",
label: String(d.Prof_Count),
color: "rgba(3,129,179,1)"
}));
const notProficient = shared_dataFiltered2.map(d => ({
entity: d.ENTITY_NAME,
x1: d.Prof_Pct,
x2: d.Prof_Pct + d.NotProf_Pct,
category: "Not Proficient",
label: String(d.NotProf_Count),
color: "rgba(170,74,68,1)"
}));
const notTested = shared_dataFiltered2.map(d => ({
entity: d.ENTITY_NAME,
x1: d.Prof_Pct + d.NotProf_Pct,
x2: d.Prof_Pct + d.NotProf_Pct + d.NotTested_Pct,
category: "Not Tested",
label: String(d.NOT_TESTED),
color: "rgba(209,209,209,1)"
}));
const segments = [...proficient, ...notProficient, ...notTested];
const entityOrder = shared_dataFiltered2.map(d => d.ENTITY_NAME).reverse();
return Plot.plot({
width,
height,
marginLeft: 175, // adjust to fit longest entity name
marginRight: 60,
marginTop: 15,
marginBottom: 40,
style: {
fontSize: "14px" // Changes all plot text sizes
},
title: cohortPlot2,
subtitle: `County: ${inputSels.county}, Grade: ${inputSels.grade}`,
caption: html`Data Source: <a href="https://data.nysed.gov/downloads.php">NYSED Report Card Database</a><br>
(xx%) = % Proficient`,
x: {
label: "% of Students",
labelAnchor: "center",
labelArrow: "none",
domain: [0, 100]
},
y: {
label: null,
domain: entityOrder, // preserves original row order
tickSize: 0
},
color: {
domain: ["#Proficient", "#Not Proficient", "#Not Tested"],
range: ["rgba(3,129,179,1)", "rgba(170,74,68,1)", "rgba(209,209,209,1)"],
legend: true
},
marks: [
// Stacked bars drawn as rects
Plot.barX(segments, {
x1: d => d.x1,
x2: d => d.x2,
y: d => d.entity,
fill: d => d.color,
stroke: "black",
strokeWidth: 0.5
}),
// Count labels inside each bar segment
Plot.text(segments, {
// x: d => d.x1 + (d.x2 - d.x1) / 2, // center of each segment
x: d => d.x1 + 1, // start of each segment
y: d => d.entity,
text: d => d.x2 - d.x1 > 2 ? d.label : "", // hide label if segment too narrow
fill: "black",
textAnchor: "start"
}),
// text in () to the right of each bar
Plot.text(shared_dataFiltered2, {
x: 100,
y: d => d.ENTITY_NAME,
// text: d => `(${d.TOTAL_COUNT})`,
text: d =>`(${d.Prof_Pct.toFixed(1)}%)`,
fill: "black",
textAnchor: "start",
dx: 5
}),
// Vertical reference line at 95%
Plot.ruleX([95], {
stroke: "black",
strokeWidth: 1,
strokeDasharray: "4,3"
}),
// Annotation label at top of the 95% line
Plot.text([">95%, nearly everyone CAN!"], {
frameAnchor: "top-right",
dy: -12, // Adjust vertical offset to move it slightly
dx: +50 // Adjust horizontal offset to move it slightly
}),
]
});
}fig1 = {
const width = 1000;
const barHeight = 24;
const height = 150 + shared_dataFiltered1.length * barHeight;
// Build stacked segments for each row
const proficient = shared_dataFiltered1.map(d => ({
entity: d.ENTITY_NAME,
x1: 0,
x2: d.Prof_Pct,
category: "Proficient",
label: String(d.Prof_Count),
color: "rgba(3,129,179,1)"
}));
const notProficient = shared_dataFiltered1.map(d => ({
entity: d.ENTITY_NAME,
x1: d.Prof_Pct,
x2: d.Prof_Pct + d.NotProf_Pct,
category: "Not Proficient",
label: String(d.NotProf_Count),
color: "rgba(170,74,68,1)"
}));
const notTested = shared_dataFiltered1.map(d => ({
entity: d.ENTITY_NAME,
x1: d.Prof_Pct + d.NotProf_Pct,
x2: d.Prof_Pct + d.NotProf_Pct + d.NotTested_Pct,
category: "Not Tested",
label: String(d.NOT_TESTED),
color: "rgba(209,209,209,1)"
}));
const segments = [...proficient, ...notProficient, ...notTested];
const entityOrder = shared_dataFiltered1.map(d => d.ENTITY_NAME).reverse();
return Plot.plot({
width,
height,
marginLeft: 175, // adjust to fit longest entity name
marginRight: 60,
marginTop: 15,
marginBottom: 60,
style: {
fontSize: "14px" // Changes all plot text sizes
},
title: cohortPlot1,
subtitle: `County: ${inputSels.county}, Grade: ${inputSels.grade}`,
caption: html`Data Source: <a href="https://data.nysed.gov/downloads.php">NYSED Report Card Database</a><br>
(xx%) = % Proficient`,
x: {
label: "% of Students",
labelAnchor: "center",
labelArrow: "none",
domain: [0, 100]
},
y: {
label: null,
domain: entityOrder, // preserves original row order
tickSize: 0
},
color: {
domain: ["#Proficient", "#Not Proficient", "#Not Tested"],
range: ["rgba(3,129,179,1)", "rgba(170,74,68,1)", "rgba(209,209,209,1)"],
legend: true
},
marks: [
// Stacked bars drawn as rects
Plot.barX(segments, {
x1: d => d.x1,
x2: d => d.x2,
y: d => d.entity,
fill: d => d.color,
stroke: "black",
strokeWidth: 0.5
}),
// Count labels inside each bar segment
Plot.text(segments, {
// x: d => d.x1 + (d.x2 - d.x1) / 2, // center of each segment
x: d => d.x1 + 1, // start of each segment
y: d => d.entity,
text: d => d.x2 - d.x1 > 2 ? d.label : "", // hide label if segment too narrow
fill: "black",
textAnchor: "start"
}),
// Text in () to the right of each bar
Plot.text(shared_dataFiltered1, {
x: 100,
y: d => d.ENTITY_NAME,
// text: d => `(${d.TOTAL_COUNT})`,
text: d =>`(${d.Prof_Pct.toFixed(1)}%)`,
fill: "black",
textAnchor: "start",
dx: 5
}),
// Vertical reference line at 95%
Plot.ruleX([95], {
stroke: "black",
strokeWidth: 1,
strokeDasharray: "4,3"
}),
// Annotation label at top of the 95% line
Plot.text([">95%, nearly everyone CAN!"], {
frameAnchor: "top-right",
dy: -12, // Adjust vertical offset to move it slightly
dx: +50 // Adjust horizontal offset to move it slightly
}),
]
});
}