1480 UX Design Evaluation Tool
Interactive heuristic evaluation for IoT interfaces and user experiences
ux-design
heuristics
usability
evaluation
human-factors
animation
1480.1 IoT UX Evaluation Tool
NoteAbout This Tool
This interactive tool helps you evaluate IoT user interfaces and experiences using Nielsen’s 10 Usability Heuristics combined with IoT-specific UX criteria. Rate issues by severity, categorize problems, and generate comprehensive evaluation reports.
TipHow to Use
- Select an interface to evaluate (or start a new custom evaluation)
- Rate each heuristic and IoT-specific criterion using the severity scale
- Add specific issues with descriptions and categories
- View the overall usability score and priority recommendations
- Export your evaluation report for documentation
Show code
viewof uxEvaluator = {
// ===========================================================================
// UX DESIGN EVALUATION TOOL FOR IOT INTERFACES
// ===========================================================================
// Features:
// - Nielsen's 10 Usability Heuristics
// - IoT-specific UX criteria
// - Severity rating (1-4 scale)
// - Issue categorization
// - Usability score calculator (0-100)
// - Before/after comparison mode
// - Export evaluation report
// ===========================================================================
// ---------------------------------------------------------------------------
// CONFIGURATION
// ---------------------------------------------------------------------------
const config = {
width: 950,
colors: {
navy: "#2C3E50",
teal: "#16A085",
orange: "#E67E22",
gray: "#7F8C8D",
lightGray: "#ECF0F1",
white: "#FFFFFF",
red: "#E74C3C",
green: "#27AE60",
purple: "#9B59B6",
blue: "#3498DB",
yellow: "#F1C40F"
}
};
// Nielsen's 10 Usability Heuristics
const nielsenHeuristics = [
{ id: "h1", name: "Visibility of System Status", description: "System should always keep users informed through appropriate feedback within reasonable time" },
{ id: "h2", name: "Match Between System and Real World", description: "System should speak the user's language with familiar words, phrases, and concepts" },
{ id: "h3", name: "User Control and Freedom", description: "Users need a clearly marked 'emergency exit' to leave unwanted states" },
{ id: "h4", name: "Consistency and Standards", description: "Users should not have to wonder whether different words, situations, or actions mean the same thing" },
{ id: "h5", name: "Error Prevention", description: "Eliminate error-prone conditions or check and present confirmation options" },
{ id: "h6", name: "Recognition Rather Than Recall", description: "Minimize memory load by making objects, actions, and options visible" },
{ id: "h7", name: "Flexibility and Efficiency of Use", description: "Allow users to tailor frequent actions; accelerators for expert users" },
{ id: "h8", name: "Aesthetic and Minimalist Design", description: "Dialogues should not contain irrelevant or rarely needed information" },
{ id: "h9", name: "Help Users Recognize, Diagnose, and Recover from Errors", description: "Error messages should be expressed in plain language with constructive solutions" },
{ id: "h10", name: "Help and Documentation", description: "Provide help that is easy to search, focused on tasks, lists concrete steps" }
];
// IoT-specific UX criteria
const iotCriteria = [
{ id: "iot1", name: "Discoverability", description: "How easily users can find and identify IoT devices and their capabilities", category: "IoT-Specific" },
{ id: "iot2", name: "Feedback Latency", description: "Appropriate response time for device actions and status updates", category: "IoT-Specific" },
{ id: "iot3", name: "Error Recovery", description: "Clear guidance when devices fail, disconnect, or malfunction", category: "IoT-Specific" },
{ id: "iot4", name: "Multi-Device Consistency", description: "Uniform experience across different devices and form factors", category: "IoT-Specific" },
{ id: "iot5", name: "Offline Capability Indication", description: "Clear communication of device connectivity status and offline features", category: "IoT-Specific" }
];
// Severity levels
const severityLevels = [
{ value: 0, label: "No Issue", color: config.colors.green, weight: 0 },
{ value: 1, label: "Cosmetic", color: config.colors.blue, weight: 0.1 },
{ value: 2, label: "Minor", color: config.colors.yellow, weight: 0.3 },
{ value: 3, label: "Major", color: config.colors.orange, weight: 0.6 },
{ value: 4, label: "Critical", color: config.colors.red, weight: 1.0 }
];
// Sample evaluations
const sampleEvaluations = {
"smart-thermostat": {
name: "Smart Thermostat Interface",
description: "Nest-style smart thermostat with touch interface and mobile app",
ratings: {
h1: 1, h2: 0, h3: 2, h4: 1, h5: 1, h6: 0, h7: 2, h8: 0, h9: 2, h10: 3,
iot1: 1, iot2: 1, iot3: 2, iot4: 1, iot5: 2
},
issues: [
{ heuristic: "h10", severity: 3, category: "Major", description: "No contextual help for advanced scheduling features" },
{ heuristic: "iot3", severity: 2, category: "Minor", description: "Error messages during HVAC communication are too technical" },
{ heuristic: "h3", severity: 2, category: "Minor", description: "Difficult to cancel scheduled temperature changes" }
]
},
"wearable-pairing": {
name: "Wearable Device Pairing Flow",
description: "Fitness tracker Bluetooth pairing and initial setup experience",
ratings: {
h1: 2, h2: 1, h3: 3, h4: 2, h5: 2, h6: 1, h7: 3, h8: 1, h9: 3, h10: 2,
iot1: 2, iot2: 2, iot3: 3, iot4: 2, iot5: 3
},
issues: [
{ heuristic: "h3", severity: 3, category: "Major", description: "No clear way to cancel pairing mid-process" },
{ heuristic: "iot3", severity: 3, category: "Major", description: "Bluetooth errors show cryptic codes instead of actionable guidance" },
{ heuristic: "iot5", severity: 3, category: "Major", description: "No indication of what features work without phone connection" },
{ heuristic: "h1", severity: 2, category: "Minor", description: "Pairing progress indicator is vague" }
]
},
"industrial-dashboard": {
name: "Industrial IoT Dashboard",
description: "Factory floor monitoring dashboard with multiple sensor feeds",
ratings: {
h1: 1, h2: 2, h3: 1, h4: 1, h5: 1, h6: 2, h7: 1, h8: 3, h9: 2, h10: 1,
iot1: 1, iot2: 1, iot3: 2, iot4: 0, iot5: 1
},
issues: [
{ heuristic: "h8", severity: 3, category: "Major", description: "Dashboard is cluttered with rarely-used metrics" },
{ heuristic: "h6", severity: 2, category: "Minor", description: "Sensor IDs require memorization, no labels visible" },
{ heuristic: "h2", severity: 2, category: "Minor", description: "Technical jargon in alerts not understood by all operators" }
]
},
"new": {
name: "New Evaluation",
description: "Start a fresh evaluation for your own IoT interface",
ratings: {
h1: 0, h2: 0, h3: 0, h4: 0, h5: 0, h6: 0, h7: 0, h8: 0, h9: 0, h10: 0,
iot1: 0, iot2: 0, iot3: 0, iot4: 0, iot5: 0
},
issues: []
}
};
// ---------------------------------------------------------------------------
// STATE MANAGEMENT
// ---------------------------------------------------------------------------
let state = {
currentEvaluation: "smart-thermostat",
ratings: { ...sampleEvaluations["smart-thermostat"].ratings },
issues: [...sampleEvaluations["smart-thermostat"].issues],
comparisonMode: false,
comparisonRatings: null
};
// ---------------------------------------------------------------------------
// CREATE CONTAINER
// ---------------------------------------------------------------------------
const container = d3.create("div")
.style("font-family", "system-ui, -apple-system, sans-serif")
.style("max-width", `${config.width}px`)
.style("margin", "0 auto")
.style("padding", "20px")
.style("background", config.colors.white)
.style("border-radius", "12px")
.style("border", `2px solid ${config.colors.lightGray}`);
// Title
container.append("div")
.style("text-align", "center")
.style("margin-bottom", "20px")
.html(`
<h3 style="color: ${config.colors.navy}; margin: 0 0 8px 0;">IoT UX Evaluation Tool</h3>
<p style="color: ${config.colors.gray}; margin: 0; font-size: 14px;">Comprehensive heuristic evaluation for IoT interfaces</p>
`);
// ---------------------------------------------------------------------------
// EVALUATION SELECTOR
// ---------------------------------------------------------------------------
const selectorRow = container.append("div")
.style("display", "flex")
.style("gap", "15px")
.style("margin-bottom", "20px")
.style("flex-wrap", "wrap")
.style("align-items", "center");
selectorRow.append("label")
.style("font-weight", "600")
.style("color", config.colors.navy)
.text("Select Interface:");
const evaluationSelect = selectorRow.append("select")
.style("padding", "8px 12px")
.style("border-radius", "6px")
.style("border", `1px solid ${config.colors.gray}`)
.style("font-size", "14px")
.style("min-width", "250px");
Object.entries(sampleEvaluations).forEach(([key, eval_]) => {
evaluationSelect.append("option")
.attr("value", key)
.text(eval_.name);
});
const comparisonBtn = selectorRow.append("button")
.text("Enable Comparison Mode")
.style("padding", "8px 16px")
.style("border-radius", "6px")
.style("border", "none")
.style("background", config.colors.purple)
.style("color", config.colors.white)
.style("cursor", "pointer")
.style("font-size", "13px");
const exportBtn = selectorRow.append("button")
.text("Export Report")
.style("padding", "8px 16px")
.style("border-radius", "6px")
.style("border", "none")
.style("background", config.colors.teal)
.style("color", config.colors.white)
.style("cursor", "pointer")
.style("font-size", "13px");
// Evaluation description
const evalDescription = container.append("div")
.style("padding", "12px 16px")
.style("background", config.colors.lightGray)
.style("border-radius", "8px")
.style("margin-bottom", "20px")
.style("font-size", "14px")
.style("color", config.colors.navy);
// ---------------------------------------------------------------------------
// MAIN CONTENT AREA
// ---------------------------------------------------------------------------
const mainContent = container.append("div")
.style("display", "grid")
.style("grid-template-columns", "1fr 300px")
.style("gap", "20px");
// Left column: Heuristics and ratings
const leftColumn = mainContent.append("div");
// Right column: Score and recommendations
const rightColumn = mainContent.append("div");
// ---------------------------------------------------------------------------
// SCORE PANEL
// ---------------------------------------------------------------------------
const scorePanel = rightColumn.append("div")
.style("background", config.colors.lightGray)
.style("border-radius", "12px")
.style("padding", "20px")
.style("text-align", "center")
.style("margin-bottom", "20px");
scorePanel.append("div")
.style("font-size", "14px")
.style("color", config.colors.gray)
.style("margin-bottom", "10px")
.text("Overall Usability Score");
const scoreValue = scorePanel.append("div")
.style("font-size", "48px")
.style("font-weight", "bold")
.style("color", config.colors.teal)
.text("0");
const scoreLabel = scorePanel.append("div")
.style("font-size", "16px")
.style("color", config.colors.navy)
.style("font-weight", "600")
.text("Excellent");
// Score breakdown
const scoreBreakdown = scorePanel.append("div")
.style("margin-top", "15px")
.style("text-align", "left")
.style("font-size", "12px");
// ---------------------------------------------------------------------------
// SEVERITY LEGEND
// ---------------------------------------------------------------------------
const legendPanel = rightColumn.append("div")
.style("background", config.colors.white)
.style("border", `1px solid ${config.colors.lightGray}`)
.style("border-radius", "8px")
.style("padding", "15px")
.style("margin-bottom", "20px");
legendPanel.append("div")
.style("font-weight", "600")
.style("color", config.colors.navy)
.style("margin-bottom", "10px")
.style("font-size", "13px")
.text("Severity Scale");
severityLevels.forEach(level => {
const row = legendPanel.append("div")
.style("display", "flex")
.style("align-items", "center")
.style("gap", "8px")
.style("margin-bottom", "6px")
.style("font-size", "12px");
row.append("div")
.style("width", "20px")
.style("height", "20px")
.style("border-radius", "4px")
.style("background", level.color)
.style("display", "flex")
.style("align-items", "center")
.style("justify-content", "center")
.style("color", config.colors.white)
.style("font-weight", "bold")
.style("font-size", "11px")
.text(level.value);
row.append("span")
.style("color", config.colors.navy)
.text(level.label);
});
// ---------------------------------------------------------------------------
// PRIORITY MATRIX
// ---------------------------------------------------------------------------
const priorityPanel = rightColumn.append("div")
.style("background", config.colors.white)
.style("border", `1px solid ${config.colors.lightGray}`)
.style("border-radius", "8px")
.style("padding", "15px");
priorityPanel.append("div")
.style("font-weight", "600")
.style("color", config.colors.navy)
.style("margin-bottom", "10px")
.style("font-size", "13px")
.text("Priority Recommendations");
const priorityList = priorityPanel.append("div")
.style("font-size", "12px");
// ---------------------------------------------------------------------------
// NIELSEN'S HEURISTICS SECTION
// ---------------------------------------------------------------------------
const nielsenSection = leftColumn.append("div")
.style("margin-bottom", "25px");
nielsenSection.append("div")
.style("font-weight", "600")
.style("color", config.colors.navy)
.style("font-size", "16px")
.style("margin-bottom", "15px")
.style("padding-bottom", "8px")
.style("border-bottom", `2px solid ${config.colors.teal}`)
.text("Nielsen's 10 Usability Heuristics");
const nielsenGrid = nielsenSection.append("div");
// ---------------------------------------------------------------------------
// IOT CRITERIA SECTION
// ---------------------------------------------------------------------------
const iotSection = leftColumn.append("div")
.style("margin-bottom", "25px");
iotSection.append("div")
.style("font-weight", "600")
.style("color", config.colors.navy)
.style("font-size", "16px")
.style("margin-bottom", "15px")
.style("padding-bottom", "8px")
.style("border-bottom", `2px solid ${config.colors.orange}`)
.text("IoT-Specific UX Criteria");
const iotGrid = iotSection.append("div");
// ---------------------------------------------------------------------------
// ISSUES SECTION
// ---------------------------------------------------------------------------
const issuesSection = leftColumn.append("div");
issuesSection.append("div")
.style("display", "flex")
.style("justify-content", "space-between")
.style("align-items", "center")
.style("margin-bottom", "15px")
.html(`
<div style="font-weight: 600; color: ${config.colors.navy}; font-size: 16px; padding-bottom: 8px; border-bottom: 2px solid ${config.colors.red};">
Identified Issues
</div>
`);
const addIssueBtn = issuesSection.select("div").append("button")
.text("+ Add Issue")
.style("padding", "6px 12px")
.style("border-radius", "4px")
.style("border", "none")
.style("background", config.colors.red)
.style("color", config.colors.white)
.style("cursor", "pointer")
.style("font-size", "12px");
const issuesList = issuesSection.append("div");
// ---------------------------------------------------------------------------
// HELPER FUNCTIONS
// ---------------------------------------------------------------------------
function calculateScore() {
const allCriteria = [...nielsenHeuristics, ...iotCriteria];
let totalWeight = 0;
let weightedDeductions = 0;
allCriteria.forEach(criterion => {
const rating = state.ratings[criterion.id] || 0;
const severity = severityLevels[rating];
totalWeight += 1;
weightedDeductions += severity.weight;
});
// Add issues impact
state.issues.forEach(issue => {
weightedDeductions += severityLevels[issue.severity].weight * 0.5;
});
const maxDeduction = allCriteria.length;
const score = Math.max(0, Math.round(100 - (weightedDeductions / maxDeduction) * 100));
return score;
}
function getScoreLabel(score) {
if (score >= 90) return { label: "Excellent", color: config.colors.green };
if (score >= 75) return { label: "Good", color: config.colors.teal };
if (score >= 60) return { label: "Needs Improvement", color: config.colors.yellow };
if (score >= 40) return { label: "Poor", color: config.colors.orange };
return { label: "Critical Issues", color: config.colors.red };
}
function getPriorityRecommendations() {
const allCriteria = [...nielsenHeuristics, ...iotCriteria];
const issues = [];
allCriteria.forEach(criterion => {
const rating = state.ratings[criterion.id] || 0;
if (rating >= 2) {
issues.push({
name: criterion.name,
severity: rating,
type: criterion.id.startsWith('iot') ? 'IoT' : 'Nielsen'
});
}
});
// Sort by severity (highest first)
issues.sort((a, b) => b.severity - a.severity);
return issues.slice(0, 5);
}
function renderHeuristicRow(criterion, gridContainer, isIoT = false) {
const row = gridContainer.append("div")
.style("display", "grid")
.style("grid-template-columns", "1fr auto")
.style("gap", "15px")
.style("padding", "12px")
.style("margin-bottom", "8px")
.style("background", config.colors.lightGray)
.style("border-radius", "8px")
.style("align-items", "center");
// Left side: name and description
const info = row.append("div");
info.append("div")
.style("font-weight", "600")
.style("color", config.colors.navy)
.style("font-size", "13px")
.style("margin-bottom", "4px")
.text(criterion.name);
info.append("div")
.style("font-size", "11px")
.style("color", config.colors.gray)
.style("line-height", "1.4")
.text(criterion.description);
// Right side: rating buttons
const ratingContainer = row.append("div")
.style("display", "flex")
.style("gap", "4px");
severityLevels.forEach(level => {
const btn = ratingContainer.append("button")
.attr("data-criterion", criterion.id)
.attr("data-value", level.value)
.style("width", "28px")
.style("height", "28px")
.style("border-radius", "4px")
.style("border", "2px solid transparent")
.style("cursor", "pointer")
.style("font-size", "12px")
.style("font-weight", "bold")
.style("transition", "all 0.2s ease")
.text(level.value);
const isSelected = state.ratings[criterion.id] === level.value;
btn.style("background", isSelected ? level.color : config.colors.white)
.style("color", isSelected ? config.colors.white : config.colors.gray)
.style("border-color", isSelected ? level.color : config.colors.lightGray);
btn.on("click", function() {
state.ratings[criterion.id] = level.value;
updateUI();
});
btn.on("mouseenter", function() {
if (state.ratings[criterion.id] !== level.value) {
d3.select(this)
.style("border-color", level.color)
.style("background", d3.color(level.color).brighter(1.5));
}
});
btn.on("mouseleave", function() {
if (state.ratings[criterion.id] !== level.value) {
d3.select(this)
.style("border-color", config.colors.lightGray)
.style("background", config.colors.white);
}
});
});
}
function renderIssue(issue, index) {
const heuristic = [...nielsenHeuristics, ...iotCriteria].find(h => h.id === issue.heuristic);
const severity = severityLevels[issue.severity];
const issueCard = issuesList.append("div")
.style("display", "grid")
.style("grid-template-columns", "auto 1fr auto")
.style("gap", "12px")
.style("padding", "12px")
.style("margin-bottom", "8px")
.style("background", config.colors.lightGray)
.style("border-radius", "8px")
.style("border-left", `4px solid ${severity.color}`)
.style("align-items", "start");
// Severity badge
issueCard.append("div")
.style("background", severity.color)
.style("color", config.colors.white)
.style("padding", "4px 8px")
.style("border-radius", "4px")
.style("font-size", "11px")
.style("font-weight", "bold")
.text(issue.category);
// Issue details
const details = issueCard.append("div");
details.append("div")
.style("font-weight", "600")
.style("color", config.colors.navy)
.style("font-size", "12px")
.style("margin-bottom", "4px")
.text(heuristic ? heuristic.name : "General Issue");
details.append("div")
.style("font-size", "12px")
.style("color", config.colors.gray)
.text(issue.description);
// Delete button
const deleteBtn = issueCard.append("button")
.text("x")
.style("width", "24px")
.style("height", "24px")
.style("border-radius", "4px")
.style("border", "none")
.style("background", config.colors.red)
.style("color", config.colors.white)
.style("cursor", "pointer")
.style("font-size", "14px");
deleteBtn.on("click", () => {
state.issues.splice(index, 1);
updateUI();
});
}
function showAddIssueModal() {
// Simple modal for adding issues
const modal = container.append("div")
.style("position", "fixed")
.style("top", "0")
.style("left", "0")
.style("right", "0")
.style("bottom", "0")
.style("background", "rgba(0,0,0,0.5)")
.style("display", "flex")
.style("align-items", "center")
.style("justify-content", "center")
.style("z-index", "1000");
const modalContent = modal.append("div")
.style("background", config.colors.white)
.style("padding", "25px")
.style("border-radius", "12px")
.style("max-width", "450px")
.style("width", "90%");
modalContent.append("h3")
.style("color", config.colors.navy)
.style("margin", "0 0 20px 0")
.text("Add New Issue");
// Heuristic selector
modalContent.append("label")
.style("display", "block")
.style("margin-bottom", "5px")
.style("font-size", "13px")
.style("color", config.colors.navy)
.text("Related Heuristic:");
const heuristicSelect = modalContent.append("select")
.style("width", "100%")
.style("padding", "8px")
.style("border-radius", "6px")
.style("border", `1px solid ${config.colors.gray}`)
.style("margin-bottom", "15px");
[...nielsenHeuristics, ...iotCriteria].forEach(h => {
heuristicSelect.append("option")
.attr("value", h.id)
.text(h.name);
});
// Severity selector
modalContent.append("label")
.style("display", "block")
.style("margin-bottom", "5px")
.style("font-size", "13px")
.style("color", config.colors.navy)
.text("Severity:");
const severitySelect = modalContent.append("select")
.style("width", "100%")
.style("padding", "8px")
.style("border-radius", "6px")
.style("border", `1px solid ${config.colors.gray}`)
.style("margin-bottom", "15px");
severityLevels.slice(1).forEach(s => {
severitySelect.append("option")
.attr("value", s.value)
.text(`${s.value} - ${s.label}`);
});
// Description
modalContent.append("label")
.style("display", "block")
.style("margin-bottom", "5px")
.style("font-size", "13px")
.style("color", config.colors.navy)
.text("Description:");
const descInput = modalContent.append("textarea")
.style("width", "100%")
.style("padding", "8px")
.style("border-radius", "6px")
.style("border", `1px solid ${config.colors.gray}`)
.style("margin-bottom", "20px")
.style("min-height", "80px")
.style("resize", "vertical")
.style("box-sizing", "border-box")
.attr("placeholder", "Describe the usability issue...");
// Buttons
const btnRow = modalContent.append("div")
.style("display", "flex")
.style("gap", "10px")
.style("justify-content", "flex-end");
btnRow.append("button")
.text("Cancel")
.style("padding", "8px 16px")
.style("border-radius", "6px")
.style("border", `1px solid ${config.colors.gray}`)
.style("background", config.colors.white)
.style("cursor", "pointer")
.on("click", () => modal.remove());
btnRow.append("button")
.text("Add Issue")
.style("padding", "8px 16px")
.style("border-radius", "6px")
.style("border", "none")
.style("background", config.colors.teal)
.style("color", config.colors.white)
.style("cursor", "pointer")
.on("click", () => {
const severity = parseInt(severitySelect.property("value"));
const categoryMap = { 1: "Cosmetic", 2: "Minor", 3: "Major", 4: "Critical" };
state.issues.push({
heuristic: heuristicSelect.property("value"),
severity: severity,
category: categoryMap[severity],
description: descInput.property("value") || "No description provided"
});
modal.remove();
updateUI();
});
}
function exportReport() {
const score = calculateScore();
const scoreInfo = getScoreLabel(score);
const priorities = getPriorityRecommendations();
const evalData = sampleEvaluations[state.currentEvaluation];
let report = `UX EVALUATION REPORT
${"=".repeat(50)}
Interface: ${evalData.name}
Description: ${evalData.description}
Evaluation Date: ${new Date().toLocaleDateString()}
OVERALL SCORE: ${score}/100 (${scoreInfo.label})
${"=".repeat(50)}
NIELSEN'S 10 USABILITY HEURISTICS
${"=".repeat(50)}
`;
nielsenHeuristics.forEach(h => {
const rating = state.ratings[h.id] || 0;
const severity = severityLevels[rating];
report += `[${rating}] ${h.name}\n ${severity.label}: ${h.description}\n\n`;
});
report += `${"=".repeat(50)}
IOT-SPECIFIC CRITERIA
${"=".repeat(50)}
`;
iotCriteria.forEach(h => {
const rating = state.ratings[h.id] || 0;
const severity = severityLevels[rating];
report += `[${rating}] ${h.name}\n ${severity.label}: ${h.description}\n\n`;
});
report += `${"=".repeat(50)}
IDENTIFIED ISSUES (${state.issues.length})
${"=".repeat(50)}
`;
state.issues.forEach((issue, i) => {
const heuristic = [...nielsenHeuristics, ...iotCriteria].find(h => h.id === issue.heuristic);
report += `${i + 1}. [${issue.category}] ${heuristic ? heuristic.name : "General"}\n ${issue.description}\n\n`;
});
report += `${"=".repeat(50)}
PRIORITY RECOMMENDATIONS
${"=".repeat(50)}
`;
priorities.forEach((p, i) => {
report += `${i + 1}. ${p.name} (Severity: ${p.severity})\n`;
});
report += `\n\nGenerated by IoT UX Evaluation Tool`;
// Download as text file
const blob = new Blob([report], { type: "text/plain" });
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = `ux-evaluation-${state.currentEvaluation}-${Date.now()}.txt`;
a.click();
URL.revokeObjectURL(url);
}
function updateUI() {
// Update description
const evalData = sampleEvaluations[state.currentEvaluation];
evalDescription.html(`<strong>${evalData.name}:</strong> ${evalData.description}`);
// Update score
const score = calculateScore();
const scoreInfo = getScoreLabel(score);
scoreValue.text(score).style("color", scoreInfo.color);
scoreLabel.text(scoreInfo.label).style("color", scoreInfo.color);
// Update score breakdown
let nielsenSum = 0, iotSum = 0;
nielsenHeuristics.forEach(h => nielsenSum += state.ratings[h.id] || 0);
iotCriteria.forEach(h => iotSum += state.ratings[h.id] || 0);
scoreBreakdown.html(`
<div style="display: flex; justify-content: space-between; margin-bottom: 4px;">
<span>Nielsen Heuristics:</span>
<span style="font-weight: 600;">${nielsenSum}/${nielsenHeuristics.length * 4}</span>
</div>
<div style="display: flex; justify-content: space-between; margin-bottom: 4px;">
<span>IoT Criteria:</span>
<span style="font-weight: 600;">${iotSum}/${iotCriteria.length * 4}</span>
</div>
<div style="display: flex; justify-content: space-between;">
<span>Issues Found:</span>
<span style="font-weight: 600; color: ${config.colors.red};">${state.issues.length}</span>
</div>
`);
// Update priority recommendations
const priorities = getPriorityRecommendations();
priorityList.html("");
if (priorities.length === 0) {
priorityList.append("div")
.style("color", config.colors.green)
.style("font-style", "italic")
.text("No critical issues identified!");
} else {
priorities.forEach((p, i) => {
const severity = severityLevels[p.severity];
const item = priorityList.append("div")
.style("display", "flex")
.style("align-items", "center")
.style("gap", "8px")
.style("padding", "6px 0")
.style("border-bottom", `1px solid ${config.colors.lightGray}`);
item.append("span")
.style("background", severity.color)
.style("color", config.colors.white)
.style("width", "20px")
.style("height", "20px")
.style("border-radius", "50%")
.style("display", "flex")
.style("align-items", "center")
.style("justify-content", "center")
.style("font-size", "11px")
.style("font-weight", "bold")
.text(i + 1);
item.append("span")
.style("color", config.colors.navy)
.style("flex", "1")
.text(p.name);
item.append("span")
.style("background", p.type === 'IoT' ? config.colors.orange : config.colors.teal)
.style("color", config.colors.white)
.style("padding", "2px 6px")
.style("border-radius", "3px")
.style("font-size", "9px")
.text(p.type);
});
}
// Update heuristic ratings display
nielsenGrid.html("");
nielsenHeuristics.forEach(h => renderHeuristicRow(h, nielsenGrid, false));
iotGrid.html("");
iotCriteria.forEach(h => renderHeuristicRow(h, iotGrid, true));
// Update issues list
issuesList.html("");
state.issues.forEach((issue, i) => renderIssue(issue, i));
if (state.issues.length === 0) {
issuesList.append("div")
.style("text-align", "center")
.style("padding", "20px")
.style("color", config.colors.gray)
.style("font-style", "italic")
.text("No specific issues documented yet. Click '+ Add Issue' to add one.");
}
}
// ---------------------------------------------------------------------------
// EVENT HANDLERS
// ---------------------------------------------------------------------------
evaluationSelect.on("change", function() {
const selected = this.value;
state.currentEvaluation = selected;
state.ratings = { ...sampleEvaluations[selected].ratings };
state.issues = [...sampleEvaluations[selected].issues];
updateUI();
});
addIssueBtn.on("click", showAddIssueModal);
exportBtn.on("click", exportReport);
comparisonBtn.on("click", function() {
state.comparisonMode = !state.comparisonMode;
d3.select(this)
.text(state.comparisonMode ? "Disable Comparison" : "Enable Comparison Mode")
.style("background", state.comparisonMode ? config.colors.red : config.colors.purple);
if (state.comparisonMode) {
state.comparisonRatings = { ...state.ratings };
} else {
state.comparisonRatings = null;
}
});
// ---------------------------------------------------------------------------
// INITIAL RENDER
// ---------------------------------------------------------------------------
updateUI();
return container.node();
}1480.2 Understanding UX Evaluation for IoT
1480.2.1 Nielsen’s 10 Usability Heuristics
Jakob Nielsen’s heuristics provide a foundational framework for evaluating any user interface:
| # | Heuristic | IoT Application |
|---|---|---|
| 1 | Visibility of System Status | Device online/offline indicators, sync status |
| 2 | Match Between System and Real World | Familiar icons for home automation |
| 3 | User Control and Freedom | Cancel pairing, undo settings changes |
| 4 | Consistency and Standards | Same gestures across all smart devices |
| 5 | Error Prevention | Confirmation before critical actions |
| 6 | Recognition Rather Than Recall | Device names instead of MAC addresses |
| 7 | Flexibility and Efficiency | Shortcuts, routines, automation |
| 8 | Aesthetic and Minimalist Design | Clean dashboards, progressive disclosure |
| 9 | Help Users Recover from Errors | Clear troubleshooting for connectivity issues |
| 10 | Help and Documentation | In-app guides, contextual help |
1480.2.2 IoT-Specific UX Considerations
IoT interfaces have unique challenges beyond traditional software:
WarningIoT UX Challenges
- Distributed Interaction: Users interact across multiple devices and touchpoints
- Asynchronous Feedback: Device responses may be delayed by network conditions
- Invisible Systems: Many IoT systems work in the background with minimal visibility
- Complex Setup: Pairing, provisioning, and configuration can frustrate users
- Failure Modes: Network outages, battery depletion, and device failures require graceful handling
1480.2.3 Severity Rating Scale
The 0-4 severity scale helps prioritize issues:
| Rating | Category | Description | Action Priority |
|---|---|---|---|
| 0 | No Issue | Meets usability standards | None needed |
| 1 | Cosmetic | Minor aesthetic issue | Low priority fix |
| 2 | Minor | Causes user inconvenience | Should fix |
| 3 | Major | Significantly impacts task completion | Must fix |
| 4 | Critical | Prevents task completion or causes errors | Fix immediately |
1480.2.4 Best Practices for IoT UX
- Provide clear connectivity status - Users need to know if devices are online
- Design for offline scenarios - What happens when network fails?
- Keep response times under 1 second - Or show progress indicators
- Use consistent patterns - Across mobile, web, and physical interfaces
- Make errors actionable - Tell users HOW to fix problems, not just that they exist
1480.3 Related Topics
- User Experience Design - UX fundamentals for IoT
- Interface and Interaction Design - Design patterns
- Design Model for IoT - Comprehensive design framework
- Understanding People and Context - User research for IoT
Interactive tool created for the IoT Class Textbook - Human Factors and Interaction Module