Spaces:
Running
Running
| console.clear(); | |
| var shapeScale = 0.6; | |
| var keyedData = { | |
| pointiness_true: { | |
| name: "pointiness_true", | |
| isRounding: true, | |
| categoryName: "pointiness", | |
| categories: ["pointy", "round"], | |
| textPlacements: {}, | |
| }, | |
| pointiness_false: { | |
| name: "pointiness_false", | |
| isRounding: false, | |
| categoryName: "pointiness", | |
| categories: ["pointy", "round", "other"], | |
| textPlacements: {}, | |
| }, | |
| shape_name_true: { | |
| name: "shape_name_true", | |
| isRounding: true, | |
| categoryName: "shape_name", | |
| categories: ["circle", "triangle", "rect"], | |
| textPlacements: {}, | |
| }, | |
| shape_name_false: { | |
| name: "shape_name_false", | |
| isRounding: false, | |
| categoryName: "shape_name", | |
| categories: ["circle", "triangle", "rect", "other"], | |
| textPlacements: {}, | |
| }, | |
| size_true: { | |
| name: "size_true", | |
| isRounding: true, | |
| categoryName: "size", | |
| categories: ["small", "large"], | |
| textPlacements: {}, | |
| }, | |
| size_false: { | |
| name: "size_false", | |
| isRounding: false, | |
| categoryName: "size", | |
| categories: ["small", "large", "other"], | |
| textPlacements: {}, | |
| }, | |
| }; | |
| var data = []; | |
| for (var key in keyedData) { | |
| data.push(keyedData[key]); | |
| } | |
| var state = { | |
| selected: data[0], | |
| selectedTopIndex: 0, | |
| selectedBottomIndex: 0, | |
| }; | |
| function updateState( | |
| category, | |
| rounding, | |
| topIndex = undefined, | |
| bottomIndex = undefined | |
| ) { | |
| var key = category + "_" + rounding; | |
| state.selected = keyedData[key]; | |
| state.selectedTopIndex = topIndex; | |
| state.selectedBottomIndex = bottomIndex; | |
| } | |
| // Placements for the center labels | |
| var textPlacements = {}; | |
| var divHeight = 720; | |
| var divWidth = 850; | |
| var c = d3.conventions({ | |
| sel: d3.select(".shape-explainer").html(""), | |
| width: divWidth, | |
| height: divHeight, | |
| layers: "ds", | |
| }); | |
| var buttonHeight = 35; | |
| var buttonWidth = 200; | |
| var buttonBuffer = 15; | |
| var topRightShift = 200; | |
| var bottomRightShift = 270; | |
| function setActiveButton() { | |
| topExplainerButtonSel.classed( | |
| "explainer-active-button", | |
| (d, i) => i == state.selectedTopIndex | |
| ); | |
| bottomExplainerButtonSel.classed( | |
| "explainer-active-button", | |
| (d, i) => i == state.selectedBottomIndex | |
| ); | |
| } | |
| // Preamble text | |
| c.svg | |
| .append("text.top-explainer-text") | |
| .at({ | |
| textAnchor: "left", | |
| dominantBaseline: "top", | |
| dy: ".33em", | |
| }) | |
| .translate([0, buttonHeight / 2]) | |
| .text("All shapes are basically..."); | |
| c.svg | |
| .append("text.bottom-explainer-text") | |
| .at({ | |
| textAnchor: "left", | |
| dominantBaseline: "top", | |
| dy: ".33em", | |
| }) | |
| .translate([0, buttonHeight * 1.5 + buttonBuffer]) | |
| .text("Everything else should be labeled..."); | |
| // Buttons | |
| var topExplainerButtonSel = c.svg | |
| .appendMany("g.explainer-button", ["pointiness", "shape_name", "size"]) | |
| .at({}) | |
| .translate((d, i) => [topRightShift + i * (buttonWidth + buttonBuffer), 0]) | |
| .on("click", function (d, i) { | |
| updateState( | |
| d, | |
| state.selected.isRounding, | |
| (topIndex = i), | |
| (bottomIndex = state.selectedBottomIndex) | |
| ); | |
| setActiveButton(); | |
| moveShapes(); | |
| }); | |
| topExplainerButtonSel.append("rect").at({ | |
| height: buttonHeight, | |
| width: buttonWidth, | |
| class: "explainer-rect", | |
| }); | |
| topExplainerButtonSel | |
| .append("text") | |
| .at({ | |
| textAnchor: "middle", | |
| dy: ".33em", | |
| x: buttonWidth / 2, | |
| y: buttonHeight / 2, | |
| class: "dropdown", | |
| }) | |
| .text((d, i) => toShortValueStringDict[d]); | |
| var bottomExplainerButtonSel = c.svg | |
| .appendMany("g.explainer-button", ["true", "false"]) | |
| .at({}) | |
| .translate((d, i) => [ | |
| bottomRightShift + i * (buttonWidth + buttonBuffer), | |
| buttonHeight + buttonBuffer, | |
| ]) | |
| .on("click", function (d, i) { | |
| updateState( | |
| state.selected.categoryName, | |
| d, | |
| (topIndex = state.selectedTopIndex), | |
| (bottomIndex = i) | |
| ); | |
| setActiveButton(); | |
| moveShapes(); | |
| }); | |
| bottomExplainerButtonSel.append("rect").at({ | |
| height: buttonHeight, | |
| width: buttonWidth, | |
| class: "explainer-rect", | |
| }); | |
| bottomExplainerButtonSel | |
| .append("text") | |
| .at({ | |
| textAnchor: "middle", | |
| dy: ".33em", | |
| x: buttonWidth / 2, | |
| y: buttonHeight / 2, | |
| class: "dropdown", | |
| }) | |
| .text((d, i) => toDropdownValueRoundingStringDict[d]); | |
| var horizontalHeight = divHeight * (5 / 8); | |
| var horizontalBuffer = 50; | |
| p = d3.line()([ | |
| [horizontalBuffer, horizontalHeight], | |
| [divWidth - horizontalBuffer, horizontalHeight], | |
| ]); | |
| var horizontal = c.svg | |
| .append("path") | |
| .at({ | |
| d: p, | |
| stroke: "black", | |
| strokeWidth: 1, | |
| }) | |
| .translate([0, 0]) | |
| .style("stroke-dasharray", "5, 5"); | |
| c.svg | |
| .append("text.label-correct") | |
| .at({ | |
| x: -400, | |
| y: 90, | |
| }) | |
| .text("correctly classified") | |
| .attr("transform", "rotate(-90)"); | |
| c.svg | |
| .append("text.label-correct") | |
| .at({ | |
| x: -630, | |
| y: 90, | |
| }) | |
| .text("incorrectly classified") | |
| .attr("transform", "rotate(-90)"); | |
| // Manually make some small adjustments to where particular shapes are placed | |
| function getFineAdjustment(shape) { | |
| if ( | |
| shape.shape_name == "rt_rect" && | |
| shape.correctness == "incorrect" && | |
| shape.gt == "shaded" | |
| ) { | |
| return 4; | |
| } | |
| if ( | |
| shape.shape_name == "rect" && | |
| shape.correctness == "incorrect" && | |
| shape.gt == "unshaded" | |
| ) { | |
| return -10; | |
| } | |
| if ( | |
| shape.shape_name == "triangle" && | |
| shape.correctness == "incorrect" && | |
| shape.gt == "unshaded" | |
| ) { | |
| return 0; | |
| } | |
| if ( | |
| shape.shape_name == "rt_circle" && | |
| shape.correctness == "incorrect" && | |
| shape.size == "small" | |
| ) { | |
| return -20; | |
| } | |
| if ( | |
| shape.shape_name == "rt_triangle" && | |
| shape.correctness == "incorrect" && | |
| shape.size == "small" | |
| ) { | |
| return -20; | |
| } | |
| return 0; | |
| } | |
| function getFinalCategory(labelName, isRounding) { | |
| if (isRounding == true) { | |
| return labelName.replace("rt_", ""); | |
| } else { | |
| if (labelName.includes("rt_")) { | |
| return "other"; | |
| } else { | |
| return labelName; | |
| } | |
| } | |
| } | |
| var startingCorrectHeight = horizontalHeight - 50; | |
| var startingIncorrectHeight = horizontalHeight + 50; | |
| var maxHeight = 180; | |
| var xRowAdjustment = 50; | |
| var heightBuffer = 10; | |
| function getPathHeight(inputPath) { | |
| var placeholder = c.svg.append("path").at({ | |
| d: scaleShapePath(inputPath, shapeScale), | |
| }); | |
| var height = placeholder.node().getBBox().height; | |
| placeholder.remove(); | |
| return height + heightBuffer; | |
| } | |
| // Figure out where to put the shapes for all possible placements | |
| function generatePlacements() { | |
| for (selectionCriteria of data) { | |
| // starting X positions | |
| var nCategories = selectionCriteria.categories.length; | |
| var centerX = []; | |
| for (var i = 0; i < nCategories; i++) { | |
| var startingX = divWidth * ((i + 1) / (nCategories + 1)); | |
| centerX.push(startingX); | |
| // Track where each label should be placed using a dictionary in the data | |
| selectionCriteria["textPlacements"][ | |
| selectionCriteria.categories[i] | |
| ] = startingX; | |
| } | |
| // For keeping of track of how we place items as we go | |
| var locationParams = {}; | |
| for (categoryIdx in selectionCriteria.categories) { | |
| var categoryName = selectionCriteria.categories[categoryIdx]; | |
| locationParams[categoryName] = { | |
| correctX: centerX[categoryIdx], | |
| incorrectX: centerX[categoryIdx], | |
| lastCorrectY: startingCorrectHeight, | |
| lastIncorrectY: startingIncorrectHeight, | |
| }; | |
| } | |
| for (shape of shapeParams) { | |
| shapeCategory = getFinalCategory( | |
| shape[selectionCriteria.categoryName], | |
| selectionCriteria.isRounding | |
| ); | |
| var shapeHeight = getPathHeight(shape.path); | |
| var shapeX, | |
| shapeY = 0; | |
| if (shape.correctness == "correct") { | |
| shapeY = locationParams[shapeCategory]["lastCorrectY"]; | |
| shapeX = locationParams[shapeCategory]["correctX"]; | |
| // Check if we've reached the maximum height | |
| if ( | |
| startingCorrectHeight - | |
| locationParams[shapeCategory]["lastCorrectY"] >= | |
| maxHeight | |
| ) { | |
| // Reset height to baseline | |
| locationParams[shapeCategory]["lastCorrectY"] = | |
| startingCorrectHeight; | |
| // Move next row over | |
| locationParams[shapeCategory]["correctX"] = | |
| locationParams[shapeCategory]["correctX"] + | |
| xRowAdjustment; | |
| } else { | |
| locationParams[shapeCategory]["lastCorrectY"] += | |
| -1 * shapeHeight; | |
| } | |
| } else { | |
| shapeY = locationParams[shapeCategory]["lastIncorrectY"]; | |
| shapeX = locationParams[shapeCategory]["incorrectX"]; | |
| if ( | |
| locationParams[shapeCategory]["lastIncorrectY"] - | |
| startingIncorrectHeight >= | |
| maxHeight | |
| ) { | |
| // Reset height to baseline | |
| locationParams[shapeCategory]["lastIncorrectY"] = | |
| startingIncorrectHeight; | |
| // Move next row over | |
| locationParams[shapeCategory]["incorrectX"] = | |
| locationParams[shapeCategory]["incorrectX"] + | |
| xRowAdjustment; | |
| } else { | |
| locationParams[shapeCategory]["lastIncorrectY"] += | |
| shapeHeight; | |
| } | |
| } | |
| shapeY = shapeY + getFineAdjustment(shape); | |
| shape[selectionCriteria.name + "_X"] = shapeX; | |
| shape[selectionCriteria.name + "_Y"] = shapeY; | |
| } | |
| } | |
| } | |
| generatePlacements(); | |
| function getLocation(shape) { | |
| return [ | |
| shape[state.selected.name + "_X"], | |
| shape[state.selected.name + "_Y"], | |
| ]; | |
| } | |
| function scaleShapePath(shapePath, factor = 0.5) { | |
| var newShapePath = ""; | |
| for (var token of shapePath.split(" ")) { | |
| if (parseInt(token)) { | |
| newShapePath = newShapePath + parseInt(token) * factor; | |
| } else { | |
| newShapePath = newShapePath + token; | |
| } | |
| newShapePath = newShapePath + " "; | |
| } | |
| return newShapePath; | |
| } | |
| // Add the shapes | |
| var explainerShapeSel = c.svg | |
| .appendMany("path.shape", shapeParams) | |
| .at({ | |
| d: (d) => scaleShapePath(d.path, shapeScale), | |
| class: (d) => "gt-" + d.gt + " " + d.correctness, | |
| }) | |
| .translate(function (d) { | |
| return getLocation(d); | |
| }); | |
| explainerShapeSel.classed("is-classified", true); | |
| function getColor(d) { | |
| var scaleRowValue = d3.scaleLinear().domain([0.3, 1.0]).range([0, 1]); | |
| return d3.interpolateRdYlGn(scaleRowValue(d)); | |
| } | |
| // Retrieve the results, for coloring the label boxes | |
| function getResults() { | |
| return calculateResults( | |
| (property = state.selected.categoryName), | |
| (useGuess = state.selected.isRounding) | |
| ); | |
| } | |
| function getCategoryAccuracy(results, category) { | |
| for (var key of results) { | |
| if (key.rawCategoryName == category) { | |
| return key.accuracy; | |
| } | |
| } | |
| } | |
| // Rename "large" and "rect" | |
| function toExplainerDisplayString(categoryName) { | |
| if (categoryName == "large") { | |
| return "big"; | |
| } | |
| if (categoryName == "rect") { | |
| return "rectangle"; | |
| } | |
| return categoryName; | |
| } | |
| function getExplainerTextColor(d, i) { | |
| console.log(d == "large"); | |
| if (d == "large" && state.selected.isRounding == false) { | |
| return "#ffccd8"; | |
| } else { | |
| return "#000000"; | |
| } | |
| } | |
| function updateText() { | |
| var explainerResults = getResults(); | |
| d3.selectAll(".explainer-label-text").html(""); | |
| d3.selectAll(".explainer-label-rect").remove(); | |
| var rectHeight = 30; | |
| var rectWidth = 80; | |
| var textRect = c.svg | |
| .appendMany("rect.column-text-rect", state.selected.categories) | |
| .at({ | |
| fill: (d) => getColor(getCategoryAccuracy(explainerResults, d)), | |
| height: rectHeight, | |
| width: rectWidth, | |
| class: "explainer-label-rect", | |
| }) | |
| .translate((d) => [ | |
| state.selected.textPlacements[d] - rectWidth / 2, | |
| horizontalHeight - rectHeight / 2, | |
| ]); | |
| var text = c.svg | |
| .appendMany("text.column-text", state.selected.categories) | |
| .at({ | |
| textAnchor: "middle", | |
| dominantBaseline: "central", | |
| class: "explainer-label-text", | |
| }) | |
| .st({ | |
| fill: getExplainerTextColor, | |
| }) | |
| .text((d) => toExplainerDisplayString(d)) | |
| .translate((d) => [state.selected.textPlacements[d], horizontalHeight]); | |
| } | |
| function moveShapes() { | |
| explainerShapeSel | |
| .transition() | |
| .duration(500) | |
| .translate((d) => getLocation(d)); | |
| updateText(); | |
| } | |
| setActiveButton(); | |
| updateText(); |