Spaces:
Running
Running
window.drawWeatherGraph = function (graphSel, fig_height, fig_width){ | |
var threshold = .4 | |
var thresholds = [0, .2, .4, .6, .8, 1].map((val, i) => { | |
var isLocked = val == 0 || val == 1 | |
return {val, i, isLocked, origVal: val} | |
}) | |
var c = d3.conventions({ | |
sel: graphSel.html('').append('div'), | |
height: fig_height, | |
totalWidth: fig_width, | |
margin: {top: 100, bottom: 100} | |
}); | |
var {predictionSel, weatherGroupSel} = (function(){ | |
c.y.domain([0,9]).clamp(true); | |
// x-Axis | |
c.xAxis.ticks(5).tickFormat(d3.format('.2f')) | |
c.yAxis.ticks(0) | |
d3.drawAxis(c) | |
c.svg.select('.x') | |
.translate(-40, 1) | |
.selectAll('line').translate(20, 1) | |
// x-Axis label | |
c.svg.append('text.axis-label') | |
.translate([c.width/2, -50]) | |
.at({textAnchor: 'middle'}) | |
.at({fill: '#000', fontSize: 14}) | |
.text('Model Score'); | |
// Weather icons | |
var weatherGroupSel = c.svg.appendMany('g.weatherdata', weatherdata) | |
.translate(d => [c.x(d.score), c.y(d.h)]) | |
//.call(d3.attachTooltip) | |
// .on("mouseover", function(d) { | |
// ttSel.html(""); | |
// var gtSel = ttSel.append("div").html(`ground truth: <span>${d.label}</span>`); | |
// ttSel.classed("tt-text", true); | |
// }) | |
weatherGroupSel.append('text.icon') | |
.text(function(d,i){return emojis[d.label];}) | |
.at({fontSize: 18, textAnchor: 'middle', dy: 8}) | |
// Add prediction circles | |
weatherGroupSel.append('circle.prediction') | |
.at({cx: 0, cy: 0, r: 14, opacity: 0, fillOpacity: 0, stroke: 'red'}); | |
weatherGroupSel.append('path.prediction') | |
.at({d: d => ['M', -10, 10, 'L', 10, -10].join(' '), stroke: 'red', opacity: 0}) | |
var predictionSel = c.svg.selectAll('.prediction'); | |
return {predictionSel, weatherGroupSel} | |
})() | |
var {thresholdSel, messageSel, setThreshold} = (function(){ | |
var thresholdSel = c.svg.append('g.threshold') | |
var thresholdGroupSel = thresholdSel.append('g') | |
.call(d3.drag().on('drag', | |
() => renderThreshold(c.x.invert(d3.clamp(0, d3.event.x, c.width)))) | |
) | |
var thesholdTextSel = thresholdGroupSel.append('g.axis').append('text') | |
.at({ | |
textAnchor: 'middle', | |
dy: '.33em', | |
y: c.height + 30 | |
}) | |
.text('Threshold') | |
var rw = 16 | |
thresholdGroupSel.append('rect') | |
.at({ | |
width: rw, | |
x: -rw/2, | |
y: -10, | |
height: c.height + 30, | |
fillOpacity: .07, | |
}) | |
var pathSel = thresholdGroupSel.append('path') | |
.at({ | |
stroke: '#000', | |
strokeDasharray: '2 2', | |
fill: 'none', | |
d: `M 0 -10 V ` + (c.height + 20), | |
}) | |
var accuracyValBox = thresholdSel.append('rect.val-box') | |
.at({width: 55, height: 20, x: c.width/2 + 32.5, y: c.height + 65, rx: 3, ry: 3}) | |
var accuracySel = thresholdSel.append('text.big-text') | |
.at({x: c.width/2 - 10, y: c.height + 80, textAnchor: 'middle'}) | |
var accuracyValSel = thresholdSel.append('text.val-text') | |
.at({x: c.width/2 + 60, y: c.height + 80, textAnchor: 'middle'}) | |
var messageSel = thresholdSel.append('text.tmessage') | |
.at({x: c.width/2, y: c.height + 120, textAnchor: 'middle'}) | |
function renderThreshold(t){ | |
if (isNaN(t)) return // TODO debug this | |
thresholdGroupSel.translate(c.x(t), 0) | |
predictionSel.at({opacity: d => isClassifiedCorrectly(d, t) ? 0 : 1}) | |
var acc = d3.mean( | |
weatherdata, | |
d => isClassifiedCorrectly(d, t) | |
) | |
accuracySel.text('Accuracy: '); | |
accuracyValSel.text(d3.format('.1%')(acc)) | |
messageSel.text('Try dragging the threshold to find the highest accuracy.') | |
thesholdTextSel.text('Threshold: ' + d3.format('.2f')(t)) | |
threshold = t | |
function isClassifiedCorrectly(d,t) { | |
return d.score >= t ? d.label == 1 : d.label == 0; | |
}; | |
} | |
renderThreshold(threshold) | |
var timer = null | |
function setThreshold(newThreshold, duration){ | |
var interpolateFn = d3.interpolate(threshold, newThreshold) | |
if (timer) timer.stop() | |
timer = d3.timer(ms => { | |
var t = Math.min(ms/duration, 1) | |
if (t == 1) timer.stop() | |
renderThreshold(interpolateFn(t)) | |
}) | |
} | |
return {thresholdSel, messageSel, setThreshold} | |
})() | |
function drawTrueLegend(c){ | |
var truthAxis = c.svg.append('g').translate([fig_width + 40, 1]) | |
truthAxis.append('text.legend-title').text('Truth') // TODO: Maybe more of a label? "what actually happened?" or just remove this legend | |
.at({textAnchor: 'middle', fontWeight: 500, x: 20}) | |
truthAxis.append('g').translate([20, 40]) | |
.append('text.legend-text').text('Sunny').parent() | |
.at({fontSize: 15}) | |
.append('text').text(emojis[0]) | |
.at({fontSize: 25, x: -30, y: 5}) | |
truthAxis.append('g').translate([20, 80]) | |
.append('text.legend-text').text('Rainy').parent() | |
.at({fontSize: 15}) | |
.append('text').text(emojis[1]) | |
.at({fontSize: 25, x: -30, y: 5}) | |
} | |
drawTrueLegend(c); | |
var {thresholdsGroupSel, renderThresholds, setThresholds} = (function(){ | |
var valsCache = [] | |
var drag = d3.drag() | |
.on('drag', function(){ | |
var val = d3.clamp(0, c.x.invert(d3.mouse(c.svg.node())[0]), 1) | |
// Force thresholds to stay sorted | |
valsCache[valsCache.activeIndex] = val | |
_.sortBy(valsCache).forEach((val, i) => thresholds[i].val = val) | |
renderThresholds() | |
}) | |
.on('start', d => { | |
valsCache = thresholds.map(d => d.val) | |
valsCache.activeIndex = d.i | |
}) | |
var thresholdsGroupSel = c.svg.append('g') | |
thresholdsGroupSel.append('text.axis-label') | |
.text('Calibrated Model Score') | |
.translate([c.width/2, c.height + 50]) | |
.at({textAnchor: 'middle'}) | |
.at({fill: '#000', fontSize: 14}) | |
thresholdsSel = thresholdsGroupSel.appendMany('g.thresholds', thresholds) | |
.call(drag) | |
.st({pointerEvents: d => d.isLocked ? 'none' : ''}) | |
thresholdsSel.append('g.axis').append('text') | |
.at({ | |
textAnchor: 'middle', | |
dy: '.33em', | |
y: c.height + 20 | |
}) | |
.text(d => d3.format('.2f')(d.origVal)) | |
var rw = 16 | |
thresholdsSel.append('rect') | |
.at({ | |
width: rw, | |
x: -rw/2, | |
height: c.height + 10, | |
fillOpacity: d => d.isLocked ? 0 : .07, | |
}) | |
var pathSel = thresholdsSel.append('path') | |
.at({ | |
stroke: '#000', | |
strokeDasharray: '2 2', | |
fill: 'none', | |
}) | |
function renderThresholds(){ | |
if (thresholds.some(d => isNaN(d.val))) return | |
thresholdsSel | |
.translate(d => c.x(d.val) + .5, 0) | |
pathSel.at({ | |
d: d => [ | |
'M', 0, c.height + 10, | |
'L', 0, 0, | |
'L', c.x(d.origVal - d.val), -12, | |
].join(' ') | |
}) | |
if (window.calibrationCurve) calibrationCurve.renderBuckets() | |
} | |
renderThresholds() | |
var timer = null | |
function setThresholds(newThresholds, duration){ | |
var interpolateFns = thresholds | |
.map((d, i) => d3.interpolate(d.val, newThresholds[i])) | |
if (timer) timer.stop() | |
timer = d3.timer(ms => { | |
var t = Math.min(ms/duration, 1) | |
if (t == 1) timer.stop() | |
thresholds.forEach((d, i) => d.val = interpolateFns[i](t)) | |
renderThresholds() | |
}) | |
} | |
return {thresholdsGroupSel, renderThresholds, setThresholds} | |
})() | |
return {c, thresholdSel, messageSel, setThreshold, predictionSel, thresholds, thresholdsGroupSel, renderThresholds, setThresholds, weatherGroupSel}; | |
} | |
if (window.init) window.init() |