|
|
|
console.clear(); |
|
|
|
var ttSel = d3.select("body").selectAppend("div.tooltip.tooltip-hidden"); |
|
|
|
const columns = ["object", "n", "n correct", "accuracy"]; |
|
const rowHeight = 50; |
|
const rowWidth = 100; |
|
const buffer = 2; |
|
|
|
const classifierBlobWidth = 50; |
|
const classifierBlobHeight = 460; |
|
|
|
function drawShapesWithData(classifier) { |
|
var divHeight = classifier.class == "show-shapes" ? 250 : 490; |
|
|
|
var c = d3.conventions({ |
|
sel: d3.select("." + classifier.class).html(""), |
|
width: 1300, |
|
height: divHeight, |
|
layers: "ds", |
|
}); |
|
|
|
function runClassifier() { |
|
classifier.isClassified = true; |
|
var duration = 3000; |
|
classifierSel.classed("is-classified", true); |
|
graphResultsGroup.classed("is-classified", true); |
|
|
|
drawResults(); |
|
buttonSel.text("Reset"); |
|
|
|
var minX = d3.min(shapeParams, (d) => d.endX - 50); |
|
var timer = d3.timer((ms) => { |
|
if (!classifier.isClassified) { |
|
timer.stop(); |
|
shapeSel.classed("is-classified", false); |
|
return; |
|
} |
|
|
|
var t = d3.easeCubicInOut(ms / duration); |
|
t = d3.clamp(0, t, 1); |
|
|
|
shapeParams.forEach((d, i) => { |
|
d.x = d.startX + (d.endX - d.startX) * t; |
|
d.y = d.startY + (d.endY - d.startY) * t; |
|
d.isClassified = d.x > minX; |
|
}); |
|
|
|
shapeSel |
|
.translate((d) => [d.x, d.y]) |
|
.classed("is-classified", (d) => d.isClassified); |
|
|
|
if (t == 1) { |
|
timer.stop(); |
|
} |
|
}); |
|
} |
|
|
|
function resetClassifier() { |
|
shapeSel.translate((d) => [d.startX, d.startY]); |
|
shapeSel.classed("is-classified", false); |
|
classifier.isClassified = false; |
|
shapeSel |
|
.transition("position") |
|
.duration(0) |
|
.translate((d) => [d.startX, d.startY]); |
|
classifierSel.classed("is-classified", false); |
|
graphResultsGroup.classed("is-classified", false); |
|
if (classifier.class != "show-shapes") { |
|
classifierBlobSel.attr("opacity", 100); |
|
} |
|
|
|
drawResults(); |
|
buttonSel.text("Run Classifier"); |
|
} |
|
|
|
|
|
var buttonSel = d3 |
|
.select("." + classifier.class + "-button") |
|
.html("") |
|
.append("button#run") |
|
.at({ |
|
type: "button", |
|
class: "classifier-button", |
|
}) |
|
.text("Run Classifier") |
|
.on("click", () => { |
|
|
|
if (classifier.isClassified) { |
|
|
|
resetClassifier(); |
|
} else { |
|
runClassifier(); |
|
} |
|
}); |
|
|
|
|
|
var classifierSel = c.svg |
|
.append("g") |
|
.at({ |
|
class: "classifier", |
|
}) |
|
.translate([465, 20]); |
|
|
|
classifierSel |
|
.append("path.classifier-bg-shaded") |
|
.at({ |
|
d: classifierBgPathTop, |
|
|
|
|
|
}) |
|
.translate([-50, 0]); |
|
|
|
classifierSel |
|
.append("text.classifier-bg-text") |
|
.at({ |
|
fill: "#000", |
|
textAnchor: "middle", |
|
dominantBaseline: "central", |
|
class: "monospace", |
|
}) |
|
.text("shaded") |
|
.translate([160, 15]); |
|
|
|
classifierSel |
|
.append("path.classifier-bg-unshaded") |
|
.at({ |
|
d: classifierBgPathBottom, |
|
}) |
|
.translate([-50, 160]); |
|
|
|
classifierSel |
|
.append("text.classifier-bg-text") |
|
.at({ |
|
fill: "#000", |
|
textAnchor: "middle", |
|
dominantBaseline: "central", |
|
class: "monospace", |
|
}) |
|
.text("unshaded") |
|
.translate([160, 175]); |
|
|
|
|
|
var shapeSel = c.svg |
|
.appendMany("path.shape", shapeParams) |
|
.at({ |
|
d: (d) => d.path, |
|
class: (d) => "gt-" + d.gt + " " + d.correctness, |
|
}) |
|
.translate(function (d) { |
|
if (classifier.class == "show-shapes") { |
|
return [d.initialX + 35, d.initialY-20]; |
|
} else { |
|
return [d.startX, d.startY]; |
|
} |
|
}) |
|
.call(d3.attachTooltip) |
|
.on("mouseover", (d) => { |
|
ttSel.html(""); |
|
if (classifier.usingLabel != "none") { |
|
ttSel |
|
.append("div") |
|
.html( |
|
`<span class="left">labeled:</span> <span class="monospace right">${toPropertyString( |
|
d[classifier.usingLabel], |
|
classifier.isRounding |
|
).slice(0, -1)}</span>` |
|
); |
|
} |
|
var gtSel = ttSel |
|
.append("div") |
|
.html( |
|
`<span class="left">ground truth:</span> <span class="monospace right">${d.gt}</span>` |
|
); |
|
if (classifier.isClassified) { |
|
ttSel |
|
.append("div.labeled-row") |
|
.html( |
|
`<span class="left">classified as:</span> <span class="monospace right">${d.label}</span>` |
|
); |
|
|
|
ttSel |
|
.append("div.correct-row") |
|
.classed("is-correct-tooltip", d.correctness == "correct") |
|
.html(`<br><span>${d.correctness}ly classified</span> `); |
|
} |
|
ttSel.classed("tt-text", true); |
|
}); |
|
|
|
|
|
if (classifier.class == "show-shapes") return; |
|
|
|
|
|
var classifierBlobSel = c.svg |
|
.append("g") |
|
.at({ |
|
class: "classifier-blob", |
|
strokeWidth: 0, |
|
}) |
|
.translate([378, 20]); |
|
|
|
classifierBlobSel |
|
.append("line.classifier-blob") |
|
.at({ |
|
class: "line", |
|
x1: 27, |
|
x2: 27, |
|
y1: 0, |
|
y2: 464, |
|
stroke: "#000", |
|
strokeWidth: 1, |
|
}) |
|
.style("stroke-dasharray", "5, 5"); |
|
|
|
classifierBlobSel |
|
.append("text.classifier-blob-text") |
|
.at({ |
|
class: "classifier-blob-text monospace", |
|
textAnchor: "middle", |
|
dominantBaseline: "central", |
|
}) |
|
.text("is_shaded classifier") |
|
.attr("transform", "translate(30,480) rotate(0)"); |
|
|
|
if (classifier.class == "show-shapes") { |
|
classifierBlobSel.classed("is-classified", true); |
|
} |
|
|
|
|
|
|
|
var graphResultsGroup = c.svg |
|
.append("g") |
|
.attr("class", "results") |
|
.translate([-20, 19]); |
|
|
|
function drawResults() { |
|
|
|
summarySel = d3 |
|
.select("." + classifier.class + "-summary") |
|
.html(summaries[classifier.class]) |
|
.translate([0, 20]); |
|
summarySel.classed("summary-text", true); |
|
summarySel.classed("is-classified", classifier.isClassified); |
|
|
|
if (!classifier.isClassified) { |
|
c.layers[0].html(""); |
|
classifier.wasClassified = false; |
|
return; |
|
} |
|
|
|
|
|
|
|
results = allResults[classifier.class]; |
|
if (!results) return; |
|
|
|
|
|
|
|
function isMatch(rowName, labelName, isRounding) { |
|
|
|
if (rowName == "shape") { |
|
return true; |
|
} |
|
if (isRounding == true) { |
|
|
|
return labelName.includes(toOriginalString(rowName)) |
|
? true |
|
: false; |
|
} else { |
|
|
|
if (labelName == toOriginalString(rowName)) { |
|
return true; |
|
} else if ( |
|
labelName.includes("rt_") && |
|
rowName == "other shapes" |
|
) { |
|
return true; |
|
} |
|
return false; |
|
} |
|
} |
|
|
|
|
|
function getColor(d, i) { |
|
if (i != 3) { |
|
|
|
return "#e6e6e6"; |
|
} else { |
|
var scaleRowValue = d3 |
|
.scaleLinear() |
|
.domain([0.3, 1.0]) |
|
.range([0, 1]); |
|
return d3.interpolateRdYlGn(scaleRowValue(d)); |
|
} |
|
} |
|
|
|
|
|
function getTextColor(d, i) { |
|
if (i != 3) { |
|
|
|
return "#000000"; |
|
} else { |
|
var bgColor = getColor(d, i); |
|
if (d < 0.3) { |
|
|
|
|
|
return "#FFCCD8"; |
|
} else { |
|
|
|
|
|
return "#000000"; |
|
} |
|
} |
|
} |
|
|
|
|
|
var tableSel = c.layers[0] |
|
.html("") |
|
.raise() |
|
.st({ width: 400 }) |
|
.append("div") |
|
.translate([0, 10]) |
|
.append("table.results-table.monospace") |
|
.st({ width: 400 }); |
|
|
|
var header = tableSel |
|
.append("thead") |
|
.append("tr") |
|
.appendMany("th", columns) |
|
.text((d) => d); |
|
|
|
var rowSel = tableSel |
|
.appendMany("tr", results) |
|
.at({ |
|
class: "row monospace", |
|
}) |
|
.on("mouseover", (row) => { |
|
if (classifier.class == "default-classifier") { |
|
return; |
|
} |
|
rowSel.classed("active", (d) => d == row); |
|
shapeSel.classed("shape-row-unhighlighted", function (d) { |
|
return !isMatch( |
|
row.object, |
|
d[classifier.usingLabel], |
|
(isRounding = classifier.isRounding) |
|
); |
|
}); |
|
}) |
|
.on("mouseout", (row) => { |
|
rowSel.classed("active", function (d) { |
|
if (d == row) { |
|
return false; |
|
} |
|
}); |
|
if (classifier.isClassified) { |
|
shapeSel.classed("shape-row-unhighlighted", 0); |
|
} |
|
}); |
|
|
|
rowSel |
|
.appendMany("td", (result) => |
|
columns.map((column) => result[column]) |
|
) |
|
.text((d) => d) |
|
.st({ |
|
backgroundColor: getColor, |
|
color: getTextColor, |
|
}); |
|
|
|
header.style("opacity", 0); |
|
rowSel.style("opacity", 0); |
|
|
|
|
|
|
|
var initialDelay = classifier.wasClassified ? 0 : 2000; |
|
classifier.wasClassified = true; |
|
|
|
header |
|
.transition() |
|
.delay(initialDelay) |
|
.duration(1000) |
|
.style("opacity", 1); |
|
rowSel |
|
.transition() |
|
.delay(function (d, i) { |
|
return initialDelay + i * 200; |
|
}) |
|
.duration(1000) |
|
.style("opacity", 1); |
|
} |
|
|
|
|
|
function drawDropdown() { |
|
if (!classifier.options) return; |
|
|
|
["rounding", "category"].forEach(function (classifierType) { |
|
if (!classifier.options[classifierType]) return; |
|
var sel = d3 |
|
.select("#" + classifier.class + "-select-" + classifierType) |
|
.html(""); |
|
sel.classed("dropdown", true); |
|
sel.appendMany("option", classifier.options[classifierType]) |
|
.at({ |
|
value: function (d) { |
|
return d.value; |
|
}, |
|
}) |
|
.text((d) => d.label); |
|
sel.on("change", function () { |
|
if (classifierType == "rounding") { |
|
classifier.isRounding = toBool(this.value); |
|
} else { |
|
classifier.usingLabel = this.value; |
|
} |
|
updateResults(); |
|
drawResults(); |
|
}); |
|
}); |
|
} |
|
drawDropdown(); |
|
updateResults(); |
|
drawResults(); |
|
|
|
|
|
if ( |
|
classifier.class == "second-classifier" || |
|
classifier.class == "final-classifier" |
|
) { |
|
runClassifier(); |
|
} |
|
} |
|
|
|
|
|
function drawConclusion() { |
|
function drawNewspapers() { |
|
d3.select(".conclusion-newspapers").html(function () { |
|
var imgPath = |
|
"img/newspapers_" + |
|
document.getElementById("conclusion-select-category").value; |
|
return ( |
|
'<img src="' + |
|
imgPath + |
|
'.png" class="newspaper-image" alt="Newspapers with headlines about bias and fairness in shape data." width=400></img>' |
|
); |
|
}); |
|
} |
|
|
|
function drawInterface() { |
|
d3.select(".conclusion-interface").html(function () { |
|
var imgPath = |
|
"img/confusing_" + |
|
document.getElementById("conclusion-select-category").value; |
|
return ( |
|
'<center><img class="interface-image" width="638" height="268" src="' + |
|
imgPath + |
|
'.png" alt="A shape that is difficult to classify with several checkboxes, none of which describe the shape. Next to the interface is a text box with a single question mark in it." srcset="' + |
|
imgPath + |
|
'.svg"></img></center>' |
|
); |
|
}); |
|
} |
|
|
|
function drawConclusionSummary() { |
|
classifierSel = d3 |
|
.select(".conclusion-summary") |
|
.html(summaries["conclusion"]); |
|
classifierSel.classed("summary-text is-classified", true); |
|
} |
|
|
|
function drawDropdown() { |
|
var sel = d3.select("#conclusion-select-category").html(""); |
|
sel.classed("dropdown", true); |
|
sel.appendMany("option", conclusionOptions.category) |
|
.at({ |
|
value: function (d) { |
|
return d.value; |
|
}, |
|
}) |
|
.text((d) => d.label); |
|
|
|
sel.on("change", function (d) { |
|
makeConclusionUpdates(); |
|
}); |
|
} |
|
|
|
function makeConclusionUpdates() { |
|
updateResults(); |
|
drawNewspapers(); |
|
drawInterface(); |
|
drawConclusionSummary(); |
|
} |
|
drawDropdown(); |
|
makeConclusionUpdates(); |
|
} |
|
|
|
|
|
var classifiers = [ |
|
{ |
|
|
|
class: "show-shapes", |
|
colorBy: (d) => d.correctness, |
|
isClassified: false, |
|
isRounding: false, |
|
usingLabel: "none", |
|
}, |
|
{ |
|
class: "default-classifier", |
|
colorBy: (d) => d.correctness, |
|
isClassified: false, |
|
isRounding: false, |
|
usingLabel: "none", |
|
}, |
|
{ |
|
class: "second-classifier", |
|
colorBy: (d) => d.correctness, |
|
isClassified: false, |
|
isRounding: true, |
|
usingLabel: "shape_name", |
|
options: { |
|
rounding: [ |
|
{ label: "with their best guess", value: true }, |
|
{ label: 'as "other"', value: false }, |
|
], |
|
}, |
|
}, |
|
{ |
|
class: "final-classifier", |
|
colorBy: (d) => d.correctness, |
|
isClassified: false, |
|
isRounding: true, |
|
usingLabel: "shape_name", |
|
options: { |
|
rounding: [ |
|
{ label: "with our best guess", value: true }, |
|
{ label: 'as "other"', value: false }, |
|
], |
|
category: [ |
|
{ |
|
label: "circles, triangles, or rectangles", |
|
value: "shape_name", |
|
}, |
|
{ label: "pointy shapes or round shapes", value: "pointiness" }, |
|
{ label: "small shapes or big shapes", value: "size" }, |
|
{ label: "just shapes", value: "none" }, |
|
], |
|
}, |
|
}, |
|
]; |
|
|
|
|
|
var conclusionOptions = { |
|
category: [ |
|
{ label: "circles, triangles, and rectangles", value: "shape_name" }, |
|
{ label: "pointy shapes and round shapes", value: "pointiness" }, |
|
{ label: "small shapes and big shapes", value: "size" }, |
|
], |
|
}; |
|
|
|
classifiers.forEach(drawShapesWithData); |
|
drawConclusion(); |
|
|
|
|
|
const preloadImages = [ |
|
"img/confusing_pointiness.png", |
|
"img/confusing_pointiness.svg", |
|
"img/confusing_shape_name.png", |
|
"img/confusing_shape_name.svg", |
|
"img/confusing_size.png", |
|
"img/confusing_size.svg", |
|
"img/interface_default.png", |
|
"img/interface_default.svg", |
|
"img/interface_shape_name_false.png", |
|
"img/interface_shape_name_false.svg", |
|
"img/interface_shape_name_true.png", |
|
"img/interface_shape_name_true.svg", |
|
"img/newspapers_pointiness.png", |
|
"img/newspapers_pointiness.svg", |
|
"img/newspapers_shape_name.png", |
|
"img/newspapers_shape_name.svg", |
|
"img/newspapers_size.png", |
|
"img/newspapers_size.svg", |
|
]; |
|
|
|
d3.select(".preload-dropdown-img") |
|
.html("") |
|
.appendMany("img", preloadImages) |
|
.at({ src: (d) => d }); |
|
|