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( `labeled: ${toPropertyString( d[classifier.usingLabel], classifier.isRounding ).slice(0, -1)}` ); } var gtSel = ttSel .append("div") .html( `ground truth: ${d.gt}` ); if (classifier.isClassified) { ttSel .append("div.labeled-row") .html( `classified as: ${d.label}` ); ttSel .append("div.correct-row") .classed("is-correct-tooltip", d.correctness == "correct") .html(`
${d.correctness}ly classified `); } 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 ( 'Newspapers with headlines about bias and fairness in shape data.' ); }); } function drawInterface() { d3.select(".conclusion-interface").html(function () { var imgPath = "img/confusing_" + document.getElementById("conclusion-select-category").value; return ( '
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.
' ); }); } 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 });