Create D3 questions with radio button selections and optional explanations. Students select from options and explain their reasoning.
Use this skill when creating questions where students:
Perfect for:
Not suitable for:
Copy these from .claude/skills/question-types/snippets/:
cards/standard-card.js → createStandardCard()cards/explanation-card.js → createExplanationCard() - For reasoningcards/video-accordion.js → createVideoAccordion() - For help videoscat courses/IM-8th-Grade/modules/Unit-3/assignments/Ramp-Up-01/questions/04/attachments/chart.js
cat courses/IM-8th-Grade/modules/Unit-3/assignments/Ramp-Up-01/questions/06/attachments/chart.js
function createDefaultState() {
return {
selectedOption: null, // ID of selected option
explanation: ""
};
}
const OPTIONS = [
{ id: "opt1", text: "y = 2x + 3", display: "\\(y = 2x + 3\\)" },
{ id: "opt2", text: "y = 3x + 2", display: "\\(y = 3x + 2\\)" },
{ id: "opt3", text: "y = x + 5", display: "\\(y = x + 5\\)" },
];
function renderOptions(container) {
const optionsDiv = container.append("div")
.style("display", "flex")
.style("flex-direction", "column")
.style("gap", "12px");
OPTIONS.forEach(option => {
const isSelected = chartState.selectedOption === option.id;
const optionCard = optionsDiv.append("div")
.style("padding", "16px")
.style("border", isSelected ? "2px solid #3b82f6" : "1px solid #e5e7eb")
.style("border-radius", "12px")
.style("background", isSelected ? "#eff6ff" : "#ffffff")
.style("cursor", interactivityLocked ? "default" : "pointer")
.style("transition", "all 0.2s")
.on("click", () => {
if (interactivityLocked) return;
chartState.selectedOption = option.id;
renderOptions(container); // Re-render to show selection
sendChartState();
});
optionCard.append("div")
.style("font-size", "18px")
.html(option.display);
});
}
function renderOptions(container) {
const form = container.append("form");
OPTIONS.forEach(option => {
const label = form.append("label")
.style("display", "block")
.style("margin", "12px 0")
.style("cursor", "pointer");
label.append("input")
.attr("type", "radio")
.attr("name", "choice")
.attr("value", option.id)
.property("checked", chartState.selectedOption === option.id)
.property("disabled", interactivityLocked)
.on("change", function() {
chartState.selectedOption = this.value;
sendChartState();
});
label.append("span")
.style("margin-left", "8px")
.html(option.display);
});
}
In codebase:
createExplanationCard(d3, container, {
prompt: "Explain why you selected this option.",
value: chartState.explanation,
onChange: (value) => {
chartState.explanation = value;
sendChartState();
},
locked: interactivityLocked
});
OPTIONS.forEach(option => {
const optionCard = optionsDiv.append("div");
// Render graph or image
if (option.type === "graph") {
renderGraph(optionCard, option.data);
} else if (option.type === "image") {
optionCard.append("img").attr("src", option.imageUrl);
}
});
const optionsGrid = container.append("div")
.style("display", "grid")
.style("grid-template-columns", "repeat(2, 1fr)")
.style("gap", "16px");
createDefaultState() with selectedOption fieldsetInteractivity() to disable selection when lockedapplyInitialState() to restore selectionHover effects:
.on("mouseover", function() {
if (!interactivityLocked) {
d3.select(this).style("background", "#f9fafb");
}
})
.on("mouseout", function() {
const isSelected = /* check selection */;
d3.select(this).style("background", isSelected ? "#eff6ff" : "#ffffff");
})
Focus states for accessibility:
optionCard
.attr("tabindex", "0")
.on("keypress", (event) => {
if (event.key === "Enter" && !interactivityLocked) {
chartState.selectedOption = option.id;
renderOptions(container);
sendChartState();
}
});