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
// 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 = ({
"icon-default-maki-rectangular": {
description: "Distinguish icons from the 'maki-rectangular' set, as SVG scaled to size.",
sizes: [1.5, 1.25, 1.0, 0.85, 0.7, 0.6, 0.5, 0.4].reverse().map(x => x+"mm"),
trials: 4,
chanceLevel: 1/5,
time: 5000
},
"icon-default-maki-triangular": {
description: "Distinguish icons from the 'maki-triangular' set, as SVG scaled to size.",
sizes: [1.5, 1.25, 1.0, 0.85, 0.7, 0.6, 0.5, 0.4].reverse().map(x => x+"mm"),
trials: 4,
chanceLevel: 1/5,
time: 5000
},
"icon-default-nps-vertical": {
description: "Distinguish icons from the 'nps-vertical' set, as SVG scaled to size.",
sizes: [1.5, 1.25, 1.0, 0.85, 0.7, 0.6, 0.5, 0.4].reverse().map(x => x+"mm"),
incomplete: [0.4].map(x => x+"mm"),
trials: 3,
chanceLevel: 1/5,
time: 6000
},
"icon-default-osm-castles": {
description: "Distinguish icons from the 'osm-castles' set, as SVG scaled to size.",
sizes: [1.5, 1.25, 1.0, 0.85, 0.7, 0.6, 0.5, 0.4].reverse().map(x => x+"mm"),
incomplete: [0.4].map(x => x+"mm"),
trials: 2,
chanceLevel: 1/5,
time: 7000
},
"icon-threshold-maki-rectangular": {
description: "Distinguish icons from the 'maki-rectangular' set, rendered as threshold image (fully black/white pixels).",
sizes: pixelSizes,
incomplete: [0.34].map(x => x+"mm"),
trials: 4,
chanceLevel: 1/5,
time: 5000
},
"icon-hinted-maki-rectangular": {
description: "Distinguish icons from the 'maki-rectangular' set, manually optimized for various pixel sizes.",
sizes: pixelSizes,
incomplete: [0.34].map(x => x+"mm"),
trials: 4,
chanceLevel: 1/5,
time: 5000
},
"icon-hinted-maki-triangular": {
description: "Distinguish icons from the 'maki-triangular' set, manually optimized for various pixel sizes.",
sizes: pixelSizes,
incomplete: [0.34].map(x => x+"mm"),
trials: 4,
chanceLevel: 1/5,
time: 5000
},
"icon-enhanced-nps-vertical": {
description: "Distinguish icons from the 'nps-vertical' set, using algorithmic shape contrast enhancement.",
sizes: [1.5, 1.25, 1.0, 0.85, 0.7, 0.6, 0.5, 0.4].reverse().map(x => x+"mm"),
incomplete: [0.4].map(x => x+"mm"),
trials: 4,
chanceLevel: 1/5,
time: 8000
},
"icon-basemap-maki-triangular": {
description: "Count icons from the 'maki-triangular' set on a map.",
sizes: [1.5, 1.25, 1.0, 0.85, 0.7, 0.6, 0.5].reverse().map(x => x+"mm"),
incomplete: [0.5].map(x => x+"mm"),
trials: 4,
time: 15000
},
"icon-basemap-maki-rectangular": {
description: "Count icons from the 'maki-rectangular' set on a map.",
sizes: [1.5, 1.25, 1.0, 0.85, 0.7, 0.6, 0.5].reverse().map(x => x+"mm"),
incomplete: [0.5].map(x => x+"mm"),
trials: 4,
time: 15000
},
})
Insert cell
Insert cell
raw_data = Promise.all([
FileAttachment("user_001.json").json(),
FileAttachment("user_002.json").json(),
FileAttachment("user_003.json").json(),
FileAttachment("user_004.json").json(),
FileAttachment("user_005.json").json(),
FileAttachment("user_006@1.json").json(), // during the pilot, we used file named user_006.json, so Observable renames this file
FileAttachment("user_007.json").json(),
FileAttachment("user_008.json").json(),
FileAttachment("user_009.json").json(),
FileAttachment("user_010.json").json(),
FileAttachment("user_011.json").json(),
FileAttachment("user_012.json").json(),
FileAttachment("user_013.json").json(),
FileAttachment("user_014.json").json(),
FileAttachment("user_015.json").json(),
FileAttachment("user_016.json").json(),
FileAttachment("user_017.json").json(),
FileAttachment("user_018.json").json(),
FileAttachment("user_019.json").json(),
FileAttachment("user_020.json").json(),
FileAttachment("user_021.json").json(),
FileAttachment("user_022@1.json").json(), // user 22 has been used as a test run after a longer break
// in the experiment, and then re-run for an actual participant
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.size||t.condition.iconSize;
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 > 4 (all map sizes)", filter: (u, i) => i > 3 },
]
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] = [];
for (let u of filteredData) {
results[task] = results[task].concat(u.taskResults[task].filter(trialFilter.filter));
}
}
return results;
}
Insert cell
vizData = selectedUser == "all" ? taskResultsAll : filteredData[+selectedUser-1].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