Public
Edited
Oct 19, 2022
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
// create array for converted pixels sizes (0.02mm step)
pixelSizes = Array.from({length: 31}, (_, i) => (0.38 + i*0.02).toFixed(2)+"mm" )
Insert cell
tasks = ({
"line-arrow": {
description: "Distinguish line with embedded arrows.",
sizes: ["0.8mm","0.6mm","0.5mm","0.4mm","0.3mm","0.25mm"].reverse(),
sizeDomain: [0.1,1.0],
sizeField: "width",
trials: 4,
chanceLevel: 1/2,
time: 8000
},
"line-arrow-amplified": {
description: "Distinguish line with embedded arrows, with enhancement.",
sizes: ["0.8mm","0.6mm","0.5mm","0.4mm","0.3mm","0.25mm"].reverse(),
sizeDomain: [0.1,1.0],
sizeField: "width",
trials: 4,
chanceLevel: 1/2,
time: 8000
},
"line-arrow-amplified-align": {
description: "Distinguish line with embedded arrows, with pixel-aligned enhancement.",
sizes: ["0.8mm","0.6mm","0.5mm","0.4mm","0.3mm","0.25mm"].reverse(),
sizeDomain: [0.1,1.0],
sizeField: "width",
trials: 4,
chanceLevel: 1/2,
time: 8000
},
// line variants: baseWidth + factor
"line-variable-width": {
description: "Distinguish lines of varying width.",
sizes: ["0.5mm","0.2mm","0.1mm"].reverse(),
sizeDomain: [0.05,0.551],
binWidth: 1/20,
sizeField: "candidatesBaseWidth",
trials: 4,
chanceLevel: 1/4,
time: 9000
},
"line-parking": {
description: "Discrimitate between differen 'parking line' types (short gap).",
sizes: ["2mm","1.5mm","1mm","0.8mm","0.6mm"].reverse(),
sizeDomain: [0.4,2.2],
binWidth: 1/20,
sizeField: "width",
trials: 3,
chanceLevel: 1/4,
time: 8000
},
"line-parking-wide": {
description: "Discrimitate between differen 'parking line' types (large gap).",
sizes: ["1.5mm","1mm","0.8mm","0.6mm","0.5mm"].reverse(),
sizeDomain: [0.4,2.2],
binWidth: 1/20,
sizeField: "width",
trials: 3,
chanceLevel: 1/4,
time: 8000
},
// parallel lines: gap + width
"count-lines-parallel": {
description: "Count parallel lines in a bundle.",
sizes: ["0.12mm","0.08mm","0.05mm","0.03mm"].reverse(),
sizeField: "lineWidth",
sizeDomain: [0.02,0.131],
binWidth: 1/100,
//sizes: ["0.75mm","0.5mm","0.25mm"].reverse(),
//sizeField: "gap",
trials: 3,
chanceLevel: 1/5,
time: 12000
},
"count-lines-map": {
description: "Count lines of a specific type on a map.",
sizes: ["1mm","0.75mm","0.5mm","0.4mm","0.3mm", "0.25mm"].reverse(),
sizeDomain: [0.1,1.0],
sizeField: "lineWidth",
trials: 3,
//chanceLevel: 1/4,
time: 25000,
excludeParticipants: [5] // disproportionate amount of wrong answers at all sizes, probably misunderstood task
},

})
Insert cell
Insert cell
raw_data = Promise.all([
//FileAttachment("user_001@1.json").json(), // User 1 was final test before participants
FileAttachment("user_002@1.json").json(),
FileAttachment("user_003@1.json").json(),
FileAttachment("user_004@1.json").json(),
FileAttachment("user_005@1.json").json(),
FileAttachment("user_006.json").json(),
FileAttachment("user_007@1.json").json(),
FileAttachment("user_008@1.json").json(),
FileAttachment("user_009@1.json").json(),
FileAttachment("user_010@1.json").json(),
FileAttachment("user_011@1.json").json(),
FileAttachment("user_012@1.json").json(),
FileAttachment("user_013@1.json").json(),
FileAttachment("user_014@1.json").json(),
FileAttachment("user_015@1.json").json(),
FileAttachment("user_016@1.json").json(),
FileAttachment("user_017@1.json").json(),
FileAttachment("user_018@1.json").json(),
FileAttachment("user_019@1.json").json(),
FileAttachment("user_020@1.json").json(),
FileAttachment("user_021@1.json").json(),
FileAttachment("user_022.json").json(),
FileAttachment("user_023.json").json(),
FileAttachment("user_024.json").json(),
FileAttachment("user_025.json").json(),
FileAttachment("user_026.json").json(),
FileAttachment("user_027.json").json(),
FileAttachment("user_028.json").json(),
FileAttachment("user_029.json").json(),
])
Insert cell
stations = ({
"A": {
pixeldensity: 520,
dppx: 3
},
"B": {
pixeldensity: 807,
dppx: 2
},
"C": {
pixeldensity: 265,
dppx: 6
},
})
Insert cell
data = raw_data.map(d => {
// calculate logMAR
let snellenTrials = d.results.filter(r => r.name == "snellen" && r.trials.length > 0)[0].trials;
// get fourth from end - this is the smallest 3x successful size (task stops at incorrect response)
let snellenSize = parseFloat(snellenTrials[snellenTrials.length - 4].condition.size);
Dimension.configure({viewingDistance: 320});
d.logMAR = Math.log10(Dimension(snellenSize/5, "mm").toNumber("arcmin"));

// store survey responses for easier access
d.ageGroup = d.results[0].trials[0].response.label;
d.gender = d.results[1].trials[0].response.label;
d.vision = d.results[2].trials[0].response.label;

// group results by task
d.taskResults = {};
Object.keys(tasks).forEach(taskName => {
d.taskResults[taskName] = [];
let results = d.results.filter(r => r.name == taskName);
results.forEach((r, i) => {
r.trials.forEach(t => {
let size = t.condition[tasks[taskName].sizeField] || t.condition.size;
let station = stations[r.context.targetStation];

Dimension.configure({pixelDensity: station.pixeldensity});
d.taskResults[taskName].push(Object.assign({}, t, {
station: r.context.targetStation,
stationOrder: i+1,
ppi: station.pixeldensity,
correct: (tasks[taskName].matchResponse || matchProperties)(t.response, t.condition),
// size, including unit - convert to unified pixels if needed
//size: size.includes("px") ? parseFloat(size) * station.dppx + "px" : size,
size: size.includes("px") ? (Math.round(Dimension(size).toNumber("mm")*50)/50).toFixed(2)+"mm" : size,
// numeric size
sizeNum: Dimension(size).toNumber("mm") //parseFloat(size)
}));
});
});
});
return d;
});
Insert cell
Insert cell
participantFilters = [
{ name: "All", filter: u => true },
{ name: "Good visual acuity (logMAR < -0.05)", filter: u => u.logMAR < -0.05 },
{ name: "Highest visual acuity (logMAR < -0.25)", filter: u => u.logMAR < -0.25 },
{ name: "Medium visual acuity (-0.25 <= logMAR < -0.05)", filter: u => u.logMAR >= -0.25 && u.logMAR < -0.05 },
{ name: "Lowest visual acuity (logMAR >= -0.05)", filter: u => u.logMAR >= -0.05 },
{ name: "Male", filter: u => u.gender == "Male" },
{ name: "Female", filter: u => u.gender == "Female" },
{ name: "Age 18-25", filter: u => u.ageGroup == "18-25" },
{ name: "Age 26-35", filter: u => u.ageGroup == "26-35" },
{ name: "Age 36-45", filter: u => u.ageGroup == "36-45" },
{ name: "Age 46-55", filter: u => u.ageGroup == "46-55" },
{ name: "Age 56-65", filter: u => u.ageGroup == "56-65" },
{ name: "User ID > 9 (all map sizes)", filter: (u, i) => i > 8 },
]
Insert cell
trialFilters = [
{ name: "All", filter: t => true },
{ name: "Exclude first station", filter: t => t.stationOrder != 1 },
{ name: "Exclude last station", filter: t => t.stationOrder != 3 },
{ name: "First station", filter: t => t.stationOrder == 1 },
{ name: "Second station", filter: t => t.stationOrder == 2 },
{ name: "Third station", filter: t => t.stationOrder == 3 },
]
Insert cell
Insert cell
Insert cell
// merge all results into single array
taskResultsAll = {
let results = {};

for (let task of Object.keys(tasks)) {
results[task] = [];
let data = filteredData;

if (tasks[task].excludeParticipants) {
data = data.filter(u => !(tasks[task].excludeParticipants.includes(u.participantId)));
}
for (let u of data) {
results[task] = results[task].concat(u.taskResults[task].filter(trialFilter.filter));
}
}
return results;
}
Insert cell
vizData = selectedUser == "all" ? taskResultsAll : filteredData.find(u => u.participantId == selectedUser).taskResults
Insert cell
Insert cell
Insert cell
function matchProperties(template, obj) {
return Object.entries(template).every(([key, value]) => {
// recurse arrays and objects
if (Array.isArray(obj[key])) {
return Array.isArray(value) && matchProperties(obj[key], value);
}
if (typeof obj[key] == "object") {
return (typeof value == "object") && matchProperties(obj[key], value);
}
return obj[key] == value
});
}

Insert cell
Dimension = require("another-dimension")
Insert cell
Plot = require("@observablehq/plot@0.4")
Insert cell

Purpose-built for displays of data

Observable is your go-to platform for exploring data and creating expressive data visualizations. Use reactive JavaScript notebooks for prototyping and a collaborative canvas for visual data exploration and dashboard creation.
Learn more