Spaces:
Running
Running
console.clear(); | |
var ttSel = d3.select("body").selectAppend("div.tooltip.tooltip-hidden"); | |
// For result tables | |
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"); | |
} | |
// Add run/reset button | |
var buttonSel = d3 | |
.select("." + classifier.class + "-button") | |
.html("") | |
.append("button#run") | |
.at({ | |
type: "button", | |
class: "classifier-button", | |
}) | |
.text("Run Classifier") | |
.on("click", () => { | |
// if already classified, reset | |
if (classifier.isClassified) { | |
// Resetting | |
resetClassifier(); | |
} else { | |
runClassifier(); | |
} | |
}); | |
// Backgrounds for different classifications | |
var classifierSel = c.svg | |
.append("g") | |
.at({ | |
class: "classifier", | |
}) | |
.translate([465, 20]); | |
classifierSel | |
.append("path.classifier-bg-shaded") | |
.at({ | |
d: classifierBgPathTop, | |
// fill: "#ccc", | |
// stroke: "#000", | |
}) | |
.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]); | |
// Add the shapes themselves | |
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 we're just showing shapes, ignore everything else | |
if (classifier.class == "show-shapes") return; | |
// Add "classifier" line | |
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); | |
} | |
// Draw the results table with accuracies | |
// This will be hidden before classifier is run. | |
var graphResultsGroup = c.svg | |
.append("g") | |
.attr("class", "results") | |
.translate([-20, 19]); | |
function drawResults() { | |
// Write text summary | |
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; | |
} | |
// Access results, which are calculated in shapes.js. | |
// If there are none, draw nothing. | |
results = allResults[classifier.class]; | |
if (!results) return; | |
// Figure out which shapes should be highlighted on mouseover | |
// This depends on whether we're "rounding" edge case examples. | |
function isMatch(rowName, labelName, isRounding) { | |
// Not filtering at all | |
if (rowName == "shape") { | |
return true; | |
} | |
if (isRounding == true) { | |
// No "other" category | |
return labelName.includes(toOriginalString(rowName)) | |
? true | |
: false; | |
} else { | |
// There is an "other" category, prefixed by "rt_" | |
if (labelName == toOriginalString(rowName)) { | |
return true; | |
} else if ( | |
labelName.includes("rt_") && | |
rowName == "other shapes" | |
) { | |
return true; | |
} | |
return false; | |
} | |
} | |
// Color the last row of each table | |
function getColor(d, i) { | |
if (i != 3) { | |
// not last index | |
return "#e6e6e6"; | |
} else { | |
var scaleRowValue = d3 | |
.scaleLinear() | |
.domain([0.3, 1.0]) | |
.range([0, 1]); | |
return d3.interpolateRdYlGn(scaleRowValue(d)); | |
} | |
} | |
// Adjust text color for visibility | |
function getTextColor(d, i) { | |
if (i != 3) { | |
// not last index | |
return "#000000"; | |
} else { | |
var bgColor = getColor(d, i); | |
if (d < 0.3) { | |
// Alternative: use a brighter color? | |
// return d3.rgb(bgColor).brighter(-2); | |
return "#FFCCD8"; | |
} else { | |
// Alternative: use a darker color? | |
// return d3.rgb(bgColor).darker(2); | |
return "#000000"; | |
} | |
} | |
} | |
// Draw results table | |
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); | |
// If the classifier has already been run before, draw results right away. | |
// Otherwise, wait for other animation to run before drawing results. | |
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); | |
} | |
// Draw the dropdowns for selecting different labels | |
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(); | |
// For continuity, auto-run the second two classifiers | |
if ( | |
classifier.class == "second-classifier" || | |
classifier.class == "final-classifier" | |
) { | |
runClassifier(); | |
} | |
} | |
// Draw the "Labels Tell Stories" section | |
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.attr('select', 'circles, triangles, and rectangles'); | |
sel.on("change", function (d) { | |
makeConclusionUpdates(); | |
}); | |
} | |
function makeConclusionUpdates() { | |
updateResults(); | |
drawNewspapers(); | |
drawInterface(); | |
drawConclusionSummary(); | |
} | |
drawDropdown(); | |
makeConclusionUpdates(); | |
} | |
// Handle the parameters everywhere classifiers are drawn | |
var classifiers = [ | |
{ | |
// Just the initial display of shapes, not interactive | |
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" }, | |
], | |
}, | |
}, | |
]; | |
// "Labels Tell Stories" dropdown options | |
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(); | |
// These images are loaded invisibly so they appear seamlessly on dropdown change | |
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 }); | |