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 }); | |