Public
Edited
May 4, 2023
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
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
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
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
calibs = {
const monxyYs = calibration5.map((d) => {
return d.MonxyY.map((item) => {
return {
x: item[0],
y: item[1],
Y: item[2]
};
});
});

return calibration5.map((d, ind) => {
return {
SubjectName: d.SubjectName,
monGamma: d.MonGamma,
monXYZ: monxyYs[ind].map((c) => xyY2XYZ(c))
};
});
}
Insert cell
primRats = {
const monXYZs = data_clean.dedupe("SubjectName").select("SubjectName", "monXYZ").objects();

const primRats = [];
for (const monXYZ of monXYZs) {
const primYs = [monXYZ.monXYZ[0].Y, monXYZ.monXYZ[1].Y, monXYZ.monXYZ[2].Y];

const primTot = primYs.reduce((s, v) => (s += v), 0);
const rs = primYs.map((c) => c / primTot);

for (let c = 0; c < 3; c++) {
primRats.push({
xs: c,
vals: rs[c],
id: monXYZ.SubjectName
});
}
}

return primRats;
}
Insert cell
data = {
let valid_subids = new Set();
for (let c = 0; c < calibs.length; c++) {
valid_subids.add(calibs[c].SubjectName);
}

let d = min_motion_data3;

d = d
.filter((d) => valid_subids.has(d[0]))
.map((item) => {
const subid = item[0];
const resp = item[4];

const calib = calibs.filter((c) => c.SubjectName === subid);

return {
Experiment: "MinMotion",
SubjectName: subid.toString(),
BlockNumber: item[1],
TrialInBlock: item[2],
TotalTrials: item[3],
monGamma: calib[0].monGamma,
monXYZ: calib[0].monXYZ,
WeightFirst: resp.weight_first,
FirstColor: resp.first_col,
WeightSecond: resp.weight_second,
SecondColor: resp.second_col,
StimType: motionTypes[resp.motion_type],
ReactionTime: item[5],
DontKnow: item[6]
};
});

let df = min_flicker_data3;

df = df
.filter((d) => valid_subids.has(d[0]))
.map((item) => {
let subid = item[0];

const resp = item[4];

const calib = calibs.filter((c) => c.SubjectName === subid);

return {
Experiment: "MinFlicker",
SubjectName: subid.toString(),
BlockNumber: item[1],
TrialInBlock: item[2],
TotalTrials: item[3],
monGamma: calib[0].monGamma,
monXYZ: calib[0].monXYZ,
WeightFirst: resp.weight_first,
FirstColor: resp.first_col,
WeightSecond: resp.weight_second,
SecondColor: resp.second_col,
StimType: flickerTypes[resp.flicker_type],
ReactionTime: item[5],
DontKnow: item[6]
};
});

d = d.concat(df);

return aq.from(d);
}
Insert cell
data_clean = data
.select(aq.not("DontKnow"))
.fold(["WeightFirst", "WeightSecond"], { as: ["Flick", "Weight"] })
.fold(["FirstColor", "SecondColor"], { as: ["Color", "RGB"] })
.filter(
(d) =>
(d.Flick == "WeightFirst" && d.Color == "FirstColor") ||
(d.Flick == "WeightSecond" && d.Color == "SecondColor")
)
.derive({
R: (d) => d.RGB[0],
G: (d) => d.RGB[1],
B: (d) => d.RGB[2]
})
.select(aq.not("RGB"))
.derive({
RGBLin: (d) => {
return {
R: ((d.R * d.Weight) ** (1.0 / 2.2)) ** d.monGamma[0],
G: ((d.G * d.Weight) ** (1.0 / 2.2)) ** d.monGamma[1],
B: ((d.B * d.Weight) ** (1.0 / 2.2)) ** d.monGamma[2]
};
},
sRGBLin: aq.escape((d) => {
return {
R: ((d.R * d.Weight) ** (1.0 / 2.2)) ** monGamma_sRGB[0],
G: ((d.G * d.Weight) ** (1.0 / 2.2)) ** monGamma_sRGB[1],
B: ((d.B * d.Weight) ** (1.0 / 2.2)) ** monGamma_sRGB[2]
};
})
})
.derive({
Luminance: aq.escape((d) => {
const xyz = rgb2xyz(d.monXYZ, d.RGBLin);
return xyz.Y;
}),
Luminance_sRGB: aq.escape((d) => {
const xyz = rgb2xyz(monXYZ_sRGB, d.sRGBLin);
return xyz.Y;
})
})
.fold(["Luminance", "Luminance_sRGB"], { as: ["Calib", "Candelas"] })
.derive({
Calib: (d) => op.replace(d.Calib, "Luminance", "Calibrated")
})
.derive({
Calib: (d) => op.replace(d.Calib, "Calibrated_sRGB", "sRGB")
})
Insert cell
data_clean.view()
Insert cell
RelLumMotion = {
const RelLum = [];

const grps = data_clean
.filter((d) => d.Experiment === "MinMotion")
.groupby("SubjectName", "Calib")
.objects({ grouped: true });

for (const sid of grps.keys()) {
const grp_data = grps.get(sid);

for (const ct of grp_data.keys()) {
const grp_calib = grp_data.get(ct);

const rperc = [];
const bperc = [];
const rperc_from_pg = [];
const bperc_from_pg = [];

for (const st of ["GreenYellow", "GreenCyan", "PurpleGreen"]) {
for (let c = 0; c < grp_calib.length; c += 2) {
let w1 = grp_calib[c].Weight;
let w2 = grp_calib[c + 1].Weight;
let stype = grp_calib[c].StimType;
if (stype !== st) {
continue;
}

// w1*G = w2*(R + G)
// w2*R = w1*G - w2*G
// w2*R = G*(w1 - w2)
// R = G*(w1 - w2)/w2
if (stype === "GreenYellow") {
const ratio = (w1 - w2) / w2;
rperc.push(ratio);

// w1*G = w2*(G + B)
// w1*G = w2*G + w2*B
// (w1 - w2)*G = w2*B
// B = (w1 - w2)/w2*G
} else if (stype === "GreenCyan") {
const ratio = (w1 - w2) / w2;
bperc.push(ratio);

// w1*P = w2*G
// w1*(0.20*R + B) = w2*G
// 0.20*w1*R + w1*B = w2*G
// w1*B = w2*G - 0.20*w1*R
// w1*B = w2*G - 0.20*w1*(G*(w1_GY - w2_GY)/w2_GY)
// w1*B = G*(w2 - 0.20*w1*(w1_GY - w2_GY)/w2_GY)
// B = G*(w2 - 0.20*w1*rperc)/w1
//
// w1*P = w2*G
// w1*(0.20*R + B) = w2*G
// 0.20*w1*R + w1*B = w2*G
// 0.20*w1*R = w2*G - w1*B
// 0.20*w1*R = w2*G - w1*G*(w1_GC - w2_GC)/w2_GC
// 0.20*w1*R = G*(w2 - w1*(w1_GC - w2_GC)/w2_GC)
// R = G*(w2 - w1*(w1_GC - w2_GC)/w2_GC)/(0.20*w1)
// R = G*(w2 - w1*bperc)/(0.20*w1)
} else if (stype === "PurpleGreen") {
const r1 = (w2 - 0.2 * w1 * d3.mean(rperc)) / w1;
bperc_from_pg.push(r1);

const r2 = (w2 - w1 * d3.mean(bperc)) / (0.2 * w1);
rperc_from_pg.push(r2);
}
}
}

let rY = 0;
let gY = 0;
let bY = 0;
if (ct === "Calibrated") {
rY = grp_calib[0].monXYZ[0].Y;
gY = grp_calib[0].monXYZ[1].Y;
bY = grp_calib[0].monXYZ[2].Y;
} else {
rY = monXYZ_sRGB.Y;
gY = monXYZ_sRGB.Y;
bY = monXYZ_sRGB.Y;
}

for (let c = 0; c < rperc_from_pg.length; c++) {
// exclude PurpleGreen for now
// const rm = d3.mean([rperc[c], rperc_from_pg[c]]);
// const bm = d3.mean([bperc[c], bperc_from_pg[c]]);

const rm = rperc[c];
const bm = bperc[c];
RelLum.push({
calib: ct,
id: sid,
r: rperc[c],
r_from_pg: rperc_from_pg[c],
b: bperc[c],
b_from_pg: bperc_from_pg[c],
r_avg_est: rm,
b_avg_est: bm,
rb_ratio: rm / bm,
gr_ratio: 1.0 / rm,
rb_ratio_mon: rY / bY,
gr_ratio_mon: gY / rY
});
}
}
}

const ToPlot = aq
.from(RelLum)
.filter((d) => d.calib === "Calibrated")
.groupby("id")
.derive({
tot: (d) => d.r_avg_est + 1.0 + d.b_avg_est
})
.derive({
r_rel: (d) => d.r_avg_est / d.tot,
g_rel: (d) => 1.0 / d.tot,
b_rel: (d) => d.b_avg_est / d.tot
})
.rollup({
r_rel_mean: (d) => op.mean(d.r_rel),
g_rel_mean: (d) => op.mean(d.g_rel),
b_rel_mean: (d) => op.mean(d.b_rel)
})
.fold(["r_rel_mean", "g_rel_mean", "b_rel_mean"], { as: ["xs", "vals"] })
.derive({
xs: (d) => op.replace(d.xs, "r_rel_mean", 0)
})
.derive({
xs: (d) => op.replace(d.xs, "g_rel_mean", 1)
})
.derive({
xs: (d) => op.replace(d.xs, "b_rel_mean", 2)
})
.derive({
xs: (d) => +d.xs
});

return { RelVals: aq.from(RelLum), ToPlot: ToPlot };
}
Insert cell
RelLumMotion.ToPlot.view()
Insert cell
// sanity checks
// these would ideally be zero, if theory holds true and if observers are "perfect"
RelLumMotion.RelVals.groupby("id")
.rollup({
rmean: (d) => op.mean(d.r),
rmean_from_pg: (d) => op.mean(d.r_from_pg),
bmean: (d) => op.mean(d.b),
bmean_from_pg: (d) => op.mean(d.b_from_pg),
sane_check_1: (d) =>
op.abs(op.mean(d.r) - op.mean(d.r_from_pg)),
sane_check_2: (d) =>
op.abs(
op.mean(d.b) - op.mean(d.b_from_pg)
)
})
.view()
Insert cell
RelLumFlicker = {
const RelLum = [];

const grps = data_clean
.filter((d) => d.Experiment === "MinFlicker" && d.Calib === "Calibrated")
.groupby("SubjectName", "Calib")
.objects({ grouped: true });

for (const sid of grps.keys()) {
const grp_data = grps.get(sid);

for (const ct of grp_data.keys()) {
const grp_calib = grp_data.get(ct);

const rperc_from_xg = [];
const bperc_of_r = [];
const rperc_of_b = [];
const bperc_from_gp = [];
const bperc_from_gp_using_rp = [];

for (const st of ["RedPurple", "GrayGreen", "GreenPurple"]) {
for (let c = 0; c < grp_calib.length; c += 2) {
let w1 = grp_calib[c].Weight;
let w2 = grp_calib[c + 1].Weight;
let stype = grp_calib[c].StimType;
if (stype !== st) {
continue;
}

// w2*P = w1*R
// w2*(0.38*R + B) = w1*R
// w2*B = (w1 - 0.38*w2)*R
// B = R*(w1 - 0.38*w2)/w2

// w2*B = (w1 - 0.38*w2)*R
// R = B*w2/(w1 - 0.38*w2)
if (stype === "RedPurple") {
// "first" side was red, "second" was purple
// w1 = red, w2 = purple
const r1 = (w1 - 0.38 * w2) / w2;
bperc_of_r.push(r1);

const r2 = w2 / (w1 - 0.38 * w2);
rperc_of_b.push(r2);

// w2*X = w1*G
// w2*(R + G + B) = w1*G
// w2*(R + B) = (w1 - w2)*G
// w2*(R + (w1_RP - 0.38*w2_RP)*R/w2_RP) = (w1 - w2)*G
// R*(w2 + (w1_RP - 0.38*w2_RP)/w2_RP) = (w1 - w2)*G
// R = G*(w1 - w2)/(w2 + (w1_RP - 0.38*w2_RP)/w2_RP)
} else if (stype === "GrayGreen") {
// "first" side was green, "second" was gray
// w1 = green, w2 = gray
const ratio = (w1 - w2) / (w2 + d3.mean(bperc_of_r));
rperc_from_xg.push(ratio);

// w2*P = w1*G
// w2*(0.38*R + B) = w1*G
// w2*B = w1*G - w2*0.38*R
// w2*B = w1*G - w2*0.38*(G*(w1_XG - w2_XG)/(w2_XG + (w1_RP - 0.38*w2_RP)/w2_RP))
// w2*B = G*(w1 - w2*0.38*(w1_XG - w2_XG)/(w2_XG + (w1_RP - 0.38*w2_RP)/w2_RP))
// w2*B = G*(w1 - w2*0.38*rperc)
// B = G*(w1 - w2*0.38*rperc)/w2

// w2*B = w1*G - w2*0.38*R
// w2*B = w1*G - w2*0.38*B*w2_RP/(w1_RP - 0.38*w2_RP)
// w2*B + w2*0.38*B*w2_RP/(w1_RP - 0.38*w2_RP) = w1*G
// B = G*w1/(w2 + w2*0.38*w2_RP/(w1_RP - 0.38*w2_RP))
} else if (stype === "GreenPurple") {
// "first" side was green, "second" was purple
// w1 = green, w2 = purple
const r1 = (w1 - w2 * 0.38 * d3.mean(rperc_from_xg)) / w2;
bperc_from_gp.push(r1);

const r2 = w1 / (w2 + w2 * 0.38 * d3.mean(rperc_of_b));
bperc_from_gp_using_rp.push(r2);
}
}
}

let rY = 0;
let gY = 0;
let bY = 0;
if (ct === "Calibrated") {
rY = grp_calib[0].monXYZ[0].Y;
gY = grp_calib[0].monXYZ[1].Y;
bY = grp_calib[0].monXYZ[2].Y;
} else {
rY = monXYZ_sRGB.Y;
gY = monXYZ_sRGB.Y;
bY = monXYZ_sRGB.Y;
}

for (let c = 0; c < rperc_from_xg.length; c++) {
const bmean = d3.mean([bperc_from_gp[c], bperc_from_gp_using_rp[c]]);
RelLum.push({
calib: ct,
id: sid,
r_from_xg: rperc_from_xg[c],
b_from_gp: bperc_from_gp[c],
b_from_gp_using_rp: bperc_from_gp_using_rp[c],
b_avg_est: bmean,
b_of_r: bperc_of_r[c],
r_of_b: rperc_of_b[c],
rb_ratio: rperc_from_xg[c] / bmean,
gr_ratio: 1.0 / rperc_from_xg[c],
rb_ratio_mon: rY / bY,
gr_ratio_mon: gY / rY
});
}
}
}

const ToPlot = aq
.from(RelLum)
.filter((d) => d.calib === "Calibrated")
.groupby("id")
.derive({
tot: (d) => d.r_from_xg + 1.0 + d.b_avg_est
})
.derive({
r_rel: (d) => d.r_from_xg / d.tot,
g_rel: (d) => 1.0 / d.tot,
b_rel: (d) => d.b_avg_est / d.tot
})
.rollup({
r_rel_mean: (d) => op.mean(d.r_rel),
g_rel_mean: (d) => op.mean(d.g_rel),
b_rel_mean: (d) => op.mean(d.b_rel)
})
.fold(["r_rel_mean", "g_rel_mean", "b_rel_mean"], { as: ["xs", "vals"] })
.derive({
xs: (d) => op.replace(d.xs, "r_rel_mean", 0)
})
.derive({
xs: (d) => op.replace(d.xs, "g_rel_mean", 1)
})
.derive({
xs: (d) => op.replace(d.xs, "b_rel_mean", 2)
})
.derive({
xs: (d) => +d.xs
});

return { RelVals: aq.from(RelLum), ToPlot: ToPlot };
}
Insert cell
RelLumFlicker.ToPlot.view()
Insert cell
// sanity checks
// these would ideally be zero, if theory holds true and if observers are "perfect"
RelLumFlicker.RelVals.groupby("id")
.rollup({
rmean_from_xg: (d) => op.mean(d.r_from_xg),
bmean_from_gp: (d) => op.mean(d.b_from_gp),
bmean_from_gp_using_rp: (d) => op.mean(d.b_from_gp_using_rp),
bmean_of_r: (d) => op.mean(d.b_of_r),
rmean_of_b: (d) => op.mean(d.r_of_b),
sane_check_1: (d) =>
op.abs(op.mean(d.b_of_r) * op.mean(d.r_from_xg) - op.mean(d.b_from_gp)),
sane_check_2: (d) =>
op.abs(
op.mean(d.b_of_r) * op.mean(d.r_from_xg) - op.mean(d.b_from_gp_using_rp)
),
sane_check_3: (d) =>
op.abs(op.mean(d.b_of_r) * op.mean(d.r_from_xg) - op.mean(d.b_avg_est))
})
.view()
Insert cell
RelLumFlicker.RelVals.view()
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
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
Insert cell
Insert cell
Insert cell
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