Skip to content

Instantly share code, notes, and snippets.

@wtaysom
Last active August 29, 2015 14:04
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save wtaysom/6f7ee490ec7642de6217 to your computer and use it in GitHub Desktop.
Save wtaysom/6f7ee490ec7642de6217 to your computer and use it in GitHub Desktop.
Interactive Bayes Rule
<style type="text/css"> .posterior { color: hsl(120, 95%, 38%); } .prior { color: hsl(0, 95%, 38%); } .sensitivity { color: hsl(200, 95%, 38%); } .specificity { color: hsl(280, 95%, 38%); } </style>

See how posterior probablity depends on the prior, sensitivity, and specificity.

equation for Bayes rule $P(A|B)=\frac{P(B|A)P(A)}{P(B|A)P(A) + (1 - P(\neg{B}|\neg{A})(1 - P(A))}$

Bayes rule describes how to soundly update uncertain beliefs in light of uncertain evidence. The formulation you see above shows how three independent variables (prior, sensitivity, and specificity) determine a single dependant one (the posterior):

  • P(A|B) The posterior probabilty expresses the degree of belief you should have after observing some evidence.
  • P(A) The prior probabilty represents your guess about the hypothesis before observing the evidence.
  • P(B) The evidence itself does not need to be definitive. Bayes rule is usually formulated with P(B) as the denominator. However, I find it a little misleading because P(B) is a function of P(A). Since wiggling P(A) generally wiggles P(B) too, I prefer to expand P(B) in order to normalize on independent variables.
  • P(B|A) Sensitivity indicates the likelihood of the evidence given the hypothesis. High sensitivity means you will often see the evidence if the hypothosis is true.
  • P(¬B|¬A) Specificity is the logical inverse of sensitivity. Given the hypothesis is false, it tells us the probability of not seeing the evidence. High specificity means you will rarely see the evidence if the hypothosis is false.

The plot shows how varying each independent variable affects the posterior. For instance, the red line shows how the prior determines the posterior for sensitivity fixed at the blue dot and specificity fixed at the purple dot. Imagine a four dimensional graph of the posterior as the function of the other three variables: Prior × Sensitivity × Specificity × Posterior. Then this plot layers three two dimensional cross-sections: Prior × Posterior, Sensitivity × Posterior, Specificity × Posterior.

Controls:

  • Hover over a series to see the numeric value of the variable (and the posterior) at that point.
  • Click to set the variable to that value.
  • Double click to select the numeric value. Good for copying.
  • Drag to vary the variable.
  • Click or drag the posterior to find the minimum change to the three independent variables yielding the desired posterior value.
  • Press left or right arrow keys to nudge the selected variable lower or higher.
  • Press up or down arrow kets to nudge the posterior higher or lower.
  • Have fun discovering variations in the example text.
<!DOCTYPE html>
<meta charset="utf-8">
<title>Bayes Rule</title>
<style>
body {
margin: 0;
font: 13px Palatino, serif;
text-align: center;
}
main {
display: flex;
justify-content: center;
min-height: 510px;
min-width: 800px;
}
#details {
margin-top: 20px;
width: 250px;
height: 510px;
text-align: left;
display: flex;
flex-direction: column;
}
#example {
flex: 1;
}
#example li {
margin-left: -2em;
}
#example ul div {
margin-left: 0.5em;
}
address {
font-style: normal;
}
/** Colors **/
.posterior {
color: hsl(120, 95%, 38%);
fill: hsl(120, 95%, 38%);
stroke: hsl(120, 95%, 38%);
}
.prior {
color: hsl(0, 95%, 38%);
fill: hsl(0, 95%, 38%);
stroke: hsl(0, 95%, 38%);
}
.sensitivity {
color: hsl(200, 95%, 38%);
fill: hsl(200, 95%, 38%);
stroke: hsl(200, 95%, 38%);
}
.specificity {
color: hsl(280, 95%, 38%);
fill: hsl(280, 95%, 38%);
stroke: hsl(280, 95%, 38%);
}
/** Grid **/
.grid .tick {
stroke: lightgrey;
}
.axis path,
.axis line {
fill: none;
stroke: grey;
}
/** Backing **/
.hit-zone {
stroke: none;
fill: none;
pointer-events: visible;
}
/** Guide **/
.guide {
stroke: grey;
stroke-width: 1;
stroke-dasharray: 6, 4;
}
.guide text {
fill: grey;
stroke: none;
}
/** Series **/
.series path {
fill: none;
stroke-width: 2;
stroke-linecap: round;
}
.series circle {
stroke: none;
}
/** Slider **/
.slider .track {
fill: none;
stroke-width: 4;
stroke-linecap: round;
}
.slider .track.maximum {
stroke: lightgrey;
}
.slider .thumb {
stroke-width: 0.5;
stroke: none;
}
/** Cursor **/
main svg {
cursor: default;
}
.disable-user-select {
-webkit-touch-callout: none;
-webkit-user-select: none;
-khtml-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}
</style>
<header>
<h1>Interactive Bayes Rule</h1>
<h2>See how
<span class="posterior">posterior</span> probablity depends on the
<span class="prior">prior</span>,
<span class="sensitivity">sensitivity</span>, and
<span class="specificity">specificity</span>.
</h2>
<!--
Equation generated from $P(A|B)=\frac{P(B|A)P(A)}{P(B|A)P(A) + (1 - P(\neg{B}|\neg{A})(1 - P(A))}$ via <http://www.tlhiv.org/ltxpreview/> colored and resized.
-->
<div id="equation">
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="774px" height="84px" viewBox="0 0 187 17" version="1.1">
<defs>
<g>
<symbol overflow="visible" id="glyph0-0">
<path style="stroke:none;" d="M 3.015625 -3.15625 L 4.71875 -3.15625 C 6.125 -3.15625 7.515625 -4.1875 7.515625 -5.296875 C 7.515625 -6.078125 6.859375 -6.8125 5.546875 -6.8125 L 2.328125 -6.8125 C 2.140625 -6.8125 2.03125 -6.8125 2.03125 -6.625 C 2.03125 -6.5 2.109375 -6.5 2.3125 -6.5 C 2.4375 -6.5 2.625 -6.484375 2.734375 -6.484375 C 2.90625 -6.453125 2.953125 -6.4375 2.953125 -6.3125 C 2.953125 -6.28125 2.953125 -6.25 2.921875 -6.125 L 1.578125 -0.78125 C 1.484375 -0.390625 1.46875 -0.3125 0.671875 -0.3125 C 0.515625 -0.3125 0.40625 -0.3125 0.40625 -0.125 C 0.40625 0 0.515625 0 0.546875 0 C 0.828125 0 1.53125 -0.03125 1.8125 -0.03125 C 2.03125 -0.03125 2.25 -0.015625 2.453125 -0.015625 C 2.671875 -0.015625 2.890625 0 3.09375 0 C 3.171875 0 3.296875 0 3.296875 -0.203125 C 3.296875 -0.3125 3.203125 -0.3125 3.015625 -0.3125 C 2.65625 -0.3125 2.375 -0.3125 2.375 -0.484375 C 2.375 -0.546875 2.390625 -0.59375 2.40625 -0.65625 Z M 3.734375 -6.125 C 3.828125 -6.46875 3.84375 -6.5 4.28125 -6.5 L 5.234375 -6.5 C 6.0625 -6.5 6.59375 -6.234375 6.59375 -5.546875 C 6.59375 -5.15625 6.390625 -4.296875 6 -3.9375 C 5.5 -3.484375 4.90625 -3.40625 4.46875 -3.40625 L 3.0625 -3.40625 Z M 3.734375 -6.125 "/>
</symbol>
<symbol overflow="visible" id="glyph0-1">
<path style="stroke:none;" d="M 1.78125 -1.140625 C 1.390625 -0.484375 1 -0.34375 0.5625 -0.3125 C 0.4375 -0.296875 0.34375 -0.296875 0.34375 -0.109375 C 0.34375 -0.046875 0.40625 0 0.484375 0 C 0.75 0 1.0625 -0.03125 1.328125 -0.03125 C 1.671875 -0.03125 2.015625 0 2.328125 0 C 2.390625 0 2.515625 0 2.515625 -0.1875 C 2.515625 -0.296875 2.4375 -0.3125 2.359375 -0.3125 C 2.140625 -0.328125 1.890625 -0.40625 1.890625 -0.65625 C 1.890625 -0.78125 1.953125 -0.890625 2.03125 -1.03125 L 2.796875 -2.296875 L 5.296875 -2.296875 C 5.3125 -2.09375 5.453125 -0.734375 5.453125 -0.640625 C 5.453125 -0.34375 4.9375 -0.3125 4.734375 -0.3125 C 4.59375 -0.3125 4.5 -0.3125 4.5 -0.109375 C 4.5 0 4.609375 0 4.640625 0 C 5.046875 0 5.46875 -0.03125 5.875 -0.03125 C 6.125 -0.03125 6.765625 0 7.015625 0 C 7.0625 0 7.1875 0 7.1875 -0.203125 C 7.1875 -0.3125 7.09375 -0.3125 6.953125 -0.3125 C 6.34375 -0.3125 6.34375 -0.375 6.3125 -0.671875 L 5.703125 -6.890625 C 5.6875 -7.09375 5.6875 -7.140625 5.515625 -7.140625 C 5.359375 -7.140625 5.3125 -7.0625 5.25 -6.96875 Z M 2.984375 -2.609375 L 4.9375 -5.90625 L 5.265625 -2.609375 Z M 2.984375 -2.609375 "/>
</symbol>
<symbol overflow="visible" id="glyph0-2">
<path style="stroke:none;" d="M 1.59375 -0.78125 C 1.5 -0.390625 1.46875 -0.3125 0.6875 -0.3125 C 0.515625 -0.3125 0.421875 -0.3125 0.421875 -0.109375 C 0.421875 0 0.515625 0 0.6875 0 L 4.25 0 C 5.828125 0 7 -1.171875 7 -2.15625 C 7 -2.875 6.421875 -3.453125 5.453125 -3.5625 C 6.484375 -3.75 7.53125 -4.484375 7.53125 -5.4375 C 7.53125 -6.171875 6.875 -6.8125 5.6875 -6.8125 L 2.328125 -6.8125 C 2.140625 -6.8125 2.046875 -6.8125 2.046875 -6.609375 C 2.046875 -6.5 2.140625 -6.5 2.328125 -6.5 C 2.34375 -6.5 2.53125 -6.5 2.703125 -6.484375 C 2.875 -6.453125 2.96875 -6.453125 2.96875 -6.3125 C 2.96875 -6.28125 2.953125 -6.25 2.9375 -6.125 Z M 3.09375 -3.65625 L 3.71875 -6.125 C 3.8125 -6.46875 3.828125 -6.5 4.25 -6.5 L 5.546875 -6.5 C 6.421875 -6.5 6.625 -5.90625 6.625 -5.46875 C 6.625 -4.59375 5.765625 -3.65625 4.5625 -3.65625 Z M 2.65625 -0.3125 C 2.515625 -0.3125 2.5 -0.3125 2.4375 -0.3125 C 2.328125 -0.328125 2.296875 -0.34375 2.296875 -0.421875 C 2.296875 -0.453125 2.296875 -0.46875 2.359375 -0.640625 L 3.046875 -3.421875 L 4.921875 -3.421875 C 5.875 -3.421875 6.078125 -2.6875 6.078125 -2.265625 C 6.078125 -1.28125 5.1875 -0.3125 4 -0.3125 Z M 2.65625 -0.3125 "/>
</symbol>
<symbol overflow="visible" id="glyph1-0">
<path style="stroke:none;" d="M 3.296875 2.390625 C 3.296875 2.359375 3.296875 2.34375 3.125 2.171875 C 1.890625 0.921875 1.5625 -0.96875 1.5625 -2.5 C 1.5625 -4.234375 1.9375 -5.96875 3.171875 -7.203125 C 3.296875 -7.328125 3.296875 -7.34375 3.296875 -7.375 C 3.296875 -7.453125 3.265625 -7.484375 3.203125 -7.484375 C 3.09375 -7.484375 2.203125 -6.796875 1.609375 -5.53125 C 1.109375 -4.4375 0.984375 -3.328125 0.984375 -2.5 C 0.984375 -1.71875 1.09375 -0.515625 1.640625 0.625 C 2.25 1.84375 3.09375 2.5 3.203125 2.5 C 3.265625 2.5 3.296875 2.46875 3.296875 2.390625 Z M 3.296875 2.390625 "/>
</symbol>
<symbol overflow="visible" id="glyph1-1">
<path style="stroke:none;" d="M 2.875 -2.5 C 2.875 -3.265625 2.765625 -4.46875 2.21875 -5.609375 C 1.625 -6.828125 0.765625 -7.484375 0.671875 -7.484375 C 0.609375 -7.484375 0.5625 -7.4375 0.5625 -7.375 C 0.5625 -7.34375 0.5625 -7.328125 0.75 -7.140625 C 1.734375 -6.15625 2.296875 -4.578125 2.296875 -2.5 C 2.296875 -0.78125 1.9375 0.96875 0.703125 2.21875 C 0.5625 2.34375 0.5625 2.359375 0.5625 2.390625 C 0.5625 2.453125 0.609375 2.5 0.671875 2.5 C 0.765625 2.5 1.671875 1.8125 2.25 0.546875 C 2.765625 -0.546875 2.875 -1.65625 2.875 -2.5 Z M 2.875 -2.5 "/>
</symbol>
<symbol overflow="visible" id="glyph1-2">
<path style="stroke:none;" d="M 6.84375 -3.265625 C 7 -3.265625 7.1875 -3.265625 7.1875 -3.453125 C 7.1875 -3.65625 7 -3.65625 6.859375 -3.65625 L 0.890625 -3.65625 C 0.75 -3.65625 0.5625 -3.65625 0.5625 -3.453125 C 0.5625 -3.265625 0.75 -3.265625 0.890625 -3.265625 Z M 6.859375 -1.328125 C 7 -1.328125 7.1875 -1.328125 7.1875 -1.53125 C 7.1875 -1.71875 7 -1.71875 6.84375 -1.71875 L 0.890625 -1.71875 C 0.75 -1.71875 0.5625 -1.71875 0.5625 -1.53125 C 0.5625 -1.328125 0.75 -1.328125 0.890625 -1.328125 Z M 6.859375 -1.328125 "/>
</symbol>
<symbol overflow="visible" id="glyph2-0">
<path style="stroke:none;" d="M 1.578125 -7.125 C 1.578125 -7.296875 1.578125 -7.484375 1.390625 -7.484375 C 1.1875 -7.484375 1.1875 -7.296875 1.1875 -7.125 L 1.1875 2.140625 C 1.1875 2.3125 1.1875 2.5 1.390625 2.5 C 1.578125 2.5 1.578125 2.3125 1.578125 2.140625 Z M 1.578125 -7.125 "/>
</symbol>
<symbol overflow="visible" id="glyph3-0">
<path style="stroke:none;" d="M 2.375 -2.15625 L 3.640625 -2.15625 C 4.8125 -2.15625 5.8125 -2.90625 5.8125 -3.671875 C 5.8125 -4.265625 5.234375 -4.765625 4.28125 -4.765625 L 1.84375 -4.765625 C 1.703125 -4.765625 1.625 -4.765625 1.625 -4.609375 C 1.625 -4.515625 1.703125 -4.515625 1.859375 -4.515625 C 1.96875 -4.515625 2 -4.515625 2.125 -4.5 C 2.265625 -4.484375 2.265625 -4.46875 2.265625 -4.390625 C 2.265625 -4.390625 2.265625 -4.34375 2.25 -4.25 L 1.328125 -0.578125 C 1.25 -0.296875 1.25 -0.25 0.703125 -0.25 C 0.578125 -0.25 0.5 -0.25 0.5 -0.09375 C 0.5 -0.09375 0.5 0 0.609375 0 C 0.8125 0 1.3125 -0.03125 1.515625 -0.03125 C 1.625 -0.03125 1.875 -0.03125 2 -0.015625 C 2.125 -0.015625 2.3125 0 2.4375 0 C 2.484375 0 2.578125 0 2.578125 -0.15625 C 2.578125 -0.25 2.5 -0.25 2.359375 -0.25 C 2.359375 -0.25 2.21875 -0.25 2.09375 -0.265625 C 1.9375 -0.28125 1.9375 -0.296875 1.9375 -0.375 C 1.9375 -0.375 1.9375 -0.421875 1.96875 -0.515625 Z M 2.875 -4.265625 C 2.9375 -4.484375 2.9375 -4.515625 3.234375 -4.515625 L 4.046875 -4.515625 C 4.703125 -4.515625 5.09375 -4.3125 5.09375 -3.8125 C 5.09375 -3.609375 5.015625 -3.046875 4.671875 -2.734375 C 4.421875 -2.515625 4.015625 -2.390625 3.515625 -2.390625 L 2.40625 -2.390625 Z M 2.875 -4.265625 "/>
</symbol>
<symbol overflow="visible" id="glyph3-1">
<path style="stroke:none;" d="M 1.328125 -0.546875 C 1.265625 -0.3125 1.25 -0.25 0.703125 -0.25 C 0.59375 -0.25 0.5 -0.25 0.5 -0.109375 C 0.5 0 0.578125 0 0.703125 0 L 3.40625 0 C 4.578125 0 5.484375 -0.796875 5.484375 -1.484375 C 5.484375 -1.984375 5.03125 -2.40625 4.28125 -2.484375 C 5.125 -2.640625 5.828125 -3.171875 5.828125 -3.765625 C 5.828125 -4.296875 5.296875 -4.765625 4.390625 -4.765625 L 1.859375 -4.765625 C 1.71875 -4.765625 1.625 -4.765625 1.625 -4.609375 C 1.625 -4.515625 1.71875 -4.515625 1.859375 -4.515625 C 1.859375 -4.515625 2 -4.515625 2.125 -4.5 C 2.265625 -4.484375 2.28125 -4.46875 2.28125 -4.390625 C 2.28125 -4.390625 2.28125 -4.34375 2.25 -4.25 Z M 2.4375 -2.578125 L 2.859375 -4.265625 C 2.921875 -4.484375 2.921875 -4.515625 3.21875 -4.515625 L 4.28125 -4.515625 C 4.984375 -4.515625 5.15625 -4.046875 5.15625 -3.78125 C 5.15625 -3.21875 4.515625 -2.578125 3.578125 -2.578125 Z M 2.109375 -0.25 C 1.890625 -0.25 1.875 -0.265625 1.875 -0.328125 C 1.875 -0.328125 1.875 -0.359375 1.90625 -0.46875 L 2.390625 -2.375 L 3.875 -2.375 C 4.515625 -2.375 4.78125 -1.9375 4.78125 -1.546875 C 4.78125 -0.84375 4.0625 -0.25 3.21875 -0.25 Z M 2.109375 -0.25 "/>
</symbol>
<symbol overflow="visible" id="glyph3-2">
<path style="stroke:none;" d="M 1.4375 -0.84375 C 1.1875 -0.453125 0.96875 -0.28125 0.5625 -0.25 C 0.484375 -0.25 0.390625 -0.25 0.390625 -0.109375 C 0.390625 -0.03125 0.453125 0 0.5 0 C 0.671875 0 0.90625 -0.03125 1.09375 -0.03125 C 1.3125 -0.03125 1.609375 0 1.8125 0 C 1.84375 0 1.953125 0 1.953125 -0.15625 C 1.953125 -0.25 1.859375 -0.25 1.828125 -0.25 C 1.78125 -0.265625 1.53125 -0.265625 1.53125 -0.453125 C 1.53125 -0.546875 1.59375 -0.65625 1.625 -0.71875 L 2.1875 -1.59375 L 4.1875 -1.59375 L 4.34375 -0.4375 C 4.328125 -0.359375 4.28125 -0.25 3.875 -0.25 C 3.78125 -0.25 3.6875 -0.25 3.6875 -0.09375 C 3.6875 -0.0625 3.703125 0 3.796875 0 C 4 0 4.5 -0.03125 4.703125 -0.03125 C 4.828125 -0.03125 4.984375 -0.015625 5.109375 -0.015625 C 5.234375 -0.015625 5.375 0 5.5 0 C 5.59375 0 5.640625 -0.0625 5.640625 -0.140625 C 5.640625 -0.25 5.5625 -0.25 5.453125 -0.25 C 5.046875 -0.25 5.03125 -0.3125 5.015625 -0.46875 L 4.390625 -4.78125 C 4.375 -4.921875 4.359375 -4.96875 4.234375 -4.96875 C 4.09375 -4.96875 4.0625 -4.90625 4 -4.8125 Z M 2.359375 -1.84375 L 3.8125 -4.125 L 4.140625 -1.84375 Z M 2.359375 -1.84375 "/>
</symbol>
<symbol overflow="visible" id="glyph4-0">
<path style="stroke:none;" d="M 2.46875 -5.21875 C 1.15625 -4.296875 0.796875 -2.8125 0.796875 -1.75 C 0.796875 -0.765625 1.09375 0.765625 2.46875 1.734375 C 2.53125 1.734375 2.609375 1.734375 2.609375 1.65625 C 2.609375 1.609375 2.59375 1.59375 2.546875 1.546875 C 1.609375 0.703125 1.28125 -0.46875 1.28125 -1.734375 C 1.28125 -3.625 2 -4.546875 2.5625 -5.0625 C 2.59375 -5.09375 2.609375 -5.109375 2.609375 -5.140625 C 2.609375 -5.21875 2.53125 -5.21875 2.46875 -5.21875 Z M 2.46875 -5.21875 "/>
</symbol>
<symbol overflow="visible" id="glyph4-1">
<path style="stroke:none;" d="M 0.625 -5.21875 C 0.578125 -5.21875 0.5 -5.21875 0.5 -5.140625 C 0.5 -5.109375 0.515625 -5.09375 0.5625 -5.03125 C 1.15625 -4.484375 1.828125 -3.546875 1.828125 -1.75 C 1.828125 -0.296875 1.375 0.8125 0.625 1.484375 C 0.5 1.609375 0.5 1.609375 0.5 1.65625 C 0.5 1.6875 0.515625 1.734375 0.578125 1.734375 C 0.671875 1.734375 1.328125 1.28125 1.796875 0.40625 C 2.09375 -0.171875 2.296875 -0.921875 2.296875 -1.734375 C 2.296875 -2.71875 2 -4.25 0.625 -5.21875 Z M 0.625 -5.21875 "/>
</symbol>
<symbol overflow="visible" id="glyph4-2">
<path style="stroke:none;" d="M 3.21875 -1.578125 L 5.359375 -1.578125 C 5.453125 -1.578125 5.609375 -1.578125 5.609375 -1.734375 C 5.609375 -1.921875 5.453125 -1.921875 5.359375 -1.921875 L 3.21875 -1.921875 L 3.21875 -4.0625 C 3.21875 -4.140625 3.21875 -4.3125 3.0625 -4.3125 C 2.890625 -4.3125 2.890625 -4.15625 2.890625 -4.0625 L 2.890625 -1.921875 L 0.75 -1.921875 C 0.65625 -1.921875 0.484375 -1.921875 0.484375 -1.75 C 0.484375 -1.578125 0.640625 -1.578125 0.75 -1.578125 L 2.890625 -1.578125 L 2.890625 0.5625 C 2.890625 0.65625 2.890625 0.828125 3.046875 0.828125 C 3.21875 0.828125 3.21875 0.65625 3.21875 0.5625 Z M 3.21875 -1.578125 "/>
</symbol>
<symbol overflow="visible" id="glyph4-3">
<path style="stroke:none;" d="M 2.328125 -4.4375 C 2.328125 -4.625 2.328125 -4.625 2.125 -4.625 C 1.671875 -4.1875 1.046875 -4.1875 0.765625 -4.1875 L 0.765625 -3.9375 C 0.921875 -3.9375 1.390625 -3.9375 1.765625 -4.125 L 1.765625 -0.578125 C 1.765625 -0.34375 1.765625 -0.25 1.078125 -0.25 L 0.8125 -0.25 L 0.8125 0 C 0.9375 0 1.796875 -0.03125 2.046875 -0.03125 C 2.265625 -0.03125 3.140625 0 3.296875 0 L 3.296875 -0.25 L 3.03125 -0.25 C 2.328125 -0.25 2.328125 -0.34375 2.328125 -0.578125 Z M 2.328125 -4.4375 "/>
</symbol>
<symbol overflow="visible" id="glyph5-0">
<path style="stroke:none;" d="M 1.359375 -4.953125 C 1.359375 -5.0625 1.359375 -5.21875 1.1875 -5.21875 C 1.015625 -5.21875 1.015625 -5.0625 1.015625 -4.953125 L 1.015625 1.46875 C 1.015625 1.578125 1.015625 1.734375 1.171875 1.734375 C 1.359375 1.734375 1.359375 1.578125 1.359375 1.46875 Z M 1.359375 -4.953125 "/>
</symbol>
<symbol overflow="visible" id="glyph5-1">
<path style="stroke:none;" d="M 5.1875 -1.578125 C 5.296875 -1.578125 5.46875 -1.578125 5.46875 -1.734375 C 5.46875 -1.921875 5.296875 -1.921875 5.1875 -1.921875 L 1.03125 -1.921875 C 0.921875 -1.921875 0.75 -1.921875 0.75 -1.75 C 0.75 -1.578125 0.90625 -1.578125 1.03125 -1.578125 Z M 5.1875 -1.578125 "/>
</symbol>
<symbol overflow="visible" id="glyph5-2">
<path style="stroke:none;" d="M 4.828125 -2.25 C 4.828125 -2.484375 4.78125 -2.515625 4.546875 -2.515625 L 0.8125 -2.515625 C 0.703125 -2.515625 0.53125 -2.515625 0.53125 -2.34375 C 0.53125 -2.171875 0.703125 -2.171875 0.8125 -2.171875 L 4.484375 -2.171875 L 4.484375 -0.859375 C 4.484375 -0.765625 4.484375 -0.59375 4.640625 -0.59375 C 4.828125 -0.59375 4.828125 -0.75 4.828125 -0.859375 Z M 4.828125 -2.25 "/>
</symbol>
</g>
</defs>
<g id="surface1">
<g class="posterior">
<use xlink:href="#glyph0-0" x="-0.283984" y="10.86484"/>
</g>
<g class="posterior">
<use xlink:href="#glyph1-0" x="7.501176" y="10.86484"/>
</g>
<g class="posterior">
<use xlink:href="#glyph0-1" x="11.371096" y="10.86484"/>
</g>
<g class="posterior">
<use xlink:href="#glyph2-0" x="18.841016" y="10.86484"/>
</g>
<g class="posterior">
<use xlink:href="#glyph0-2" x="21.603906" y="10.86484"/>
</g>
<g class="posterior">
<use xlink:href="#glyph1-1" x="29.667966" y="10.86484"/>
</g>
<g>
<use xlink:href="#glyph1-2" x="36.300783" y="10.86484"/>
</g>
<g class="sensitivity">
<use xlink:href="#glyph3-0" x="94.035966" y="6.03203"/>
</g>
<g class="sensitivity">
<use xlink:href="#glyph4-0" x="100.182836" y="6.03203"/>
</g>
<g class="sensitivity">
<use xlink:href="#glyph3-1" x="103.296896" y="6.03203"/>
</g>
<g class="sensitivity">
<use xlink:href="#glyph5-0" x="109.659786" y="6.03203"/>
</g>
<g class="sensitivity">
<use xlink:href="#glyph3-2" x="112.026976" y="6.03203"/>
</g>
<g class="sensitivity">
<use xlink:href="#glyph4-1" x="118.021116" y="6.03203"/>
</g>
<g class="prior">
<use xlink:href="#glyph3-0" x="121.135176" y="6.03203"/>
</g>
<g class="prior">
<use xlink:href="#glyph4-0" x="127.282046" y="6.03203"/>
</g>
<g class="prior">
<use xlink:href="#glyph3-2" x="130.396106" y="6.03203"/>
</g>
<g class="prior">
<use xlink:href="#glyph4-1" x="136.390246" y="6.03203"/>
</g>
<path style="fill:none;stroke-width:4.05;stroke-linecap:butt;stroke-linejoin:miter;stroke:rgb(0%,0%,0%);stroke-opacity:1;stroke-miterlimit:10;" d="M 480.195312 86.328125 L 1855.117188 86.328125 " transform="matrix(0.1,0,0,-0.1,0,17)"/>
<g class="sensitivity">
<use xlink:href="#glyph3-0" x="48.0191" y="14.30312"/>
</g>
<g class="sensitivity">
<use xlink:href="#glyph4-0" x="54.16597" y="14.30312"/>
</g>
<g class="sensitivity">
<use xlink:href="#glyph3-1" x="57.28003" y="14.30312"/>
</g>
<g class="sensitivity">
<use xlink:href="#glyph5-0" x="63.64292" y="14.30312"/>
</g>
<g class="sensitivity">
<use xlink:href="#glyph3-2" x="66.01011" y="14.30312"/>
</g>
<g class="sensitivity">
<use xlink:href="#glyph4-1" x="72.00425" y="14.30312"/>
</g>
<g class="prior">
<use xlink:href="#glyph3-0" x="75.11831" y="14.30312"/>
</g>
<g class="prior">
<use xlink:href="#glyph4-0" x="81.26518" y="14.30312"/>
</g>
<g class="prior">
<use xlink:href="#glyph3-2" x="84.37924" y="14.30312"/>
</g>
<g class="prior">
<use xlink:href="#glyph4-1" x="90.37338" y="14.30312"/>
</g>
<g>
<use xlink:href="#glyph4-2" x="93.487385" y="14.30312"/>
</g>
<g>
<use xlink:href="#glyph4-0" x="99.607382" y="14.30312"/>
</g>
<g>
<use xlink:href="#glyph4-3" x="102.721382" y="14.30312"/>
</g>
<g>
<use xlink:href="#glyph5-1" x="106.69058" y="14.30312"/>
</g>
<g class="specificity">
<use xlink:href="#glyph3-0" x="112.91871" y="14.30312"/>
</g>
<g class="specificity">
<use xlink:href="#glyph4-0" x="119.06558" y="14.30312"/>
</g>
<g class="specificity">
<use xlink:href="#glyph5-2" x="122.17964" y="14.30312"/>
</g>
<g class="specificity">
<use xlink:href="#glyph3-1" x="127.55269" y="14.30312"/>
</g>
<g class="specificity">
<use xlink:href="#glyph5-0" x="133.91558" y="14.30312"/>
</g>
<g class="specificity">
<use xlink:href="#glyph5-2" x="136.282584" y="14.30312"/>
</g>
<g class="specificity">
<use xlink:href="#glyph3-2" x="141.65542" y="14.30312"/>
</g>
<g class="specificity">
<use xlink:href="#glyph4-1" x="147.64956" y="14.30312"/>
</g>
<g>
<use xlink:href="#glyph4-0" x="150.763559" y="14.30312"/>
</g>
<g>
<use xlink:href="#glyph4-3" x="153.877558" y="14.30312"/>
</g>
<g>
<use xlink:href="#glyph5-1" x="157.84646" y="14.30312"/>
</g>
<g class="prior">
<use xlink:href="#glyph3-0" x="164.07459" y="14.30312"/>
</g>
<g class="prior">
<use xlink:href="#glyph4-0" x="170.22146" y="14.30312"/>
</g>
<g class="prior">
<use xlink:href="#glyph3-2" x="173.33552" y="14.30312"/>
</g>
<g class="prior">
<use xlink:href="#glyph4-1" x="179.32966" y="14.30312"/>
</g>
<g>
<use xlink:href="#glyph4-1" x="182.443659" y="14.30312"/>
</g>
</g>
</svg>
</div>
</header>
<main>
<script>
// Directly include the 10% of d3 <http://d3js.org/> that we use.
d3 = function() {
var d3 = {
version: "3.3.11"
};
var d3_document = document, d3_documentElement = d3_document.documentElement, d3_window = window;
try {
d3_array(d3_documentElement.childNodes)[0].nodeType;
} catch (e) {
d3_array = function(list) {
var i = list.length, array = new Array(i);
while (i--) array[i] = list[i];
return array;
};
}
try {
d3_document.createElement("div").style.setProperty("opacity", 0, "");
} catch (error) {
var d3_element_prototype = d3_window.Element.prototype, d3_element_setAttribute = d3_element_prototype.setAttribute, d3_element_setAttributeNS = d3_element_prototype.setAttributeNS, d3_style_prototype = d3_window.CSSStyleDeclaration.prototype, d3_style_setProperty = d3_style_prototype.setProperty;
d3_element_prototype.setAttribute = function(name, value) {
d3_element_setAttribute.call(this, name, value + "");
};
d3_element_prototype.setAttributeNS = function(space, local, value) {
d3_element_setAttributeNS.call(this, space, local, value + "");
};
d3_style_prototype.setProperty = function(name, value, priority) {
d3_style_setProperty.call(this, name, value + "", priority);
};
}
var abs = Math.abs;
d3.range = function(start, stop, step) {
if (arguments.length < 3) {
step = 1;
if (arguments.length < 2) {
stop = start;
start = 0;
}
}
if ((stop - start) / step === Infinity) throw new Error("infinite range");
var range = [], k = d3_range_integerScale(abs(step)), i = -1, j;
start *= k, stop *= k, step *= k;
if (step < 0) while ((j = start + step * ++i) > stop) range.push(j / k); else while ((j = start + step * ++i) < stop) range.push(j / k);
return range;
};
function d3_range_integerScale(x) {
var k = 1;
while (x * k % 1) k *= 10;
return k;
}
function d3_class(ctor, properties) {
try {
for (var key in properties) {
Object.defineProperty(ctor.prototype, key, {
value: properties[key],
enumerable: false
});
}
} catch (e) {
ctor.prototype = properties;
}
}
d3.map = function(object) {
var map = new d3_Map();
if (object instanceof d3_Map) object.forEach(function(key, value) {
map.set(key, value);
}); else for (var key in object) map.set(key, object[key]);
return map;
};
function d3_Map() {}
d3_class(d3_Map, {
has: function(key) {
return d3_map_prefix + key in this;
},
get: function(key) {
return this[d3_map_prefix + key];
},
set: function(key, value) {
return this[d3_map_prefix + key] = value;
},
remove: function(key) {
key = d3_map_prefix + key;
return key in this && delete this[key];
},
forEach: function(f) {
for (var key in this) {
if (key.charCodeAt(0) === d3_map_prefixCode) {
f.call(this, key.substring(1), this[key]);
}
}
}
});
var d3_map_prefix = "\x00", d3_map_prefixCode = d3_map_prefix.charCodeAt(0);
function d3_vendorSymbol(object, name) {
if (name in object) return name;
name = name.charAt(0).toUpperCase() + name.substring(1);
for (var i = 0, n = d3_vendorPrefixes.length; i < n; ++i) {
var prefixName = d3_vendorPrefixes[i] + name;
if (prefixName in object) return prefixName;
}
}
var d3_vendorPrefixes = [ "webkit", "ms", "moz", "Moz", "o", "O" ];
d3.event = null;
function d3_eventPreventDefault() {
d3.event.preventDefault();
}
function d3_eventSource() {
var e = d3.event, s;
while (s = e.sourceEvent) e = s;
return e;
}
d3.requote = function(s) {
return s.replace(d3_requote_re, "\\$&");
};
var d3_requote_re = /[\\\^\$\*\+\?\|\[\]\(\)\.\{\}]/g;
var d3_subclass = {}.__proto__ ? function(object, prototype) {
object.__proto__ = prototype;
} : function(object, prototype) {
for (var property in prototype) object[property] = prototype[property];
};
function d3_selection(groups) {
d3_subclass(groups, d3_selectionPrototype);
return groups;
}
var d3_select = function(s, n) {
return n.querySelector(s);
}, d3_selectAll = function(s, n) {
return n.querySelectorAll(s);
}, d3_selectMatcher = d3_documentElement[d3_vendorSymbol(d3_documentElement, "matchesSelector")], d3_selectMatches = function(n, s) {
return d3_selectMatcher.call(n, s);
};
if (typeof Sizzle === "function") {
d3_select = function(s, n) {
return Sizzle(s, n)[0] || null;
};
d3_selectAll = function(s, n) {
return Sizzle.uniqueSort(Sizzle(s, n));
};
d3_selectMatches = Sizzle.matchesSelector;
}
d3.selection = function() {
return d3_selectionRoot;
};
var d3_selectionPrototype = d3.selection.prototype = [];
d3_selectionPrototype.select = function(selector) {
var subgroups = [], subgroup, subnode, group, node;
selector = d3_selection_selector(selector);
for (var j = -1, m = this.length; ++j < m; ) {
subgroups.push(subgroup = []);
subgroup.parentNode = (group = this[j]).parentNode;
for (var i = -1, n = group.length; ++i < n; ) {
if (node = group[i]) {
subgroup.push(subnode = selector.call(node, node.__data__, i, j));
if (subnode && "__data__" in node) subnode.__data__ = node.__data__;
} else {
subgroup.push(null);
}
}
}
return d3_selection(subgroups);
};
function d3_selection_selector(selector) {
return typeof selector === "function" ? selector : function() {
return d3_select(selector, this);
};
}
d3_selectionPrototype.selectAll = function(selector) {
var subgroups = [], subgroup, node;
selector = d3_selection_selectorAll(selector);
for (var j = -1, m = this.length; ++j < m; ) {
for (var group = this[j], i = -1, n = group.length; ++i < n; ) {
if (node = group[i]) {
subgroups.push(subgroup = d3_array(selector.call(node, node.__data__, i, j)));
subgroup.parentNode = node;
}
}
}
return d3_selection(subgroups);
};
function d3_selection_selectorAll(selector) {
return typeof selector === "function" ? selector : function() {
return d3_selectAll(selector, this);
};
}
var d3_nsPrefix = {
svg: "http://www.w3.org/2000/svg",
xhtml: "http://www.w3.org/1999/xhtml",
xlink: "http://www.w3.org/1999/xlink",
xml: "http://www.w3.org/XML/1998/namespace",
xmlns: "http://www.w3.org/2000/xmlns/"
};
d3.ns = {
prefix: d3_nsPrefix,
qualify: function(name) {
var i = name.indexOf(":"), prefix = name;
if (i >= 0) {
prefix = name.substring(0, i);
name = name.substring(i + 1);
}
return d3_nsPrefix.hasOwnProperty(prefix) ? {
space: d3_nsPrefix[prefix],
local: name
} : name;
}
};
d3_selectionPrototype.attr = function(name, value) {
if (arguments.length < 2) {
if (typeof name === "string") {
var node = this.node();
name = d3.ns.qualify(name);
return name.local ? node.getAttributeNS(name.space, name.local) : node.getAttribute(name);
}
for (value in name) this.each(d3_selection_attr(value, name[value]));
return this;
}
return this.each(d3_selection_attr(name, value));
};
function d3_selection_attr(name, value) {
name = d3.ns.qualify(name);
function attrNull() {
this.removeAttribute(name);
}
function attrNullNS() {
this.removeAttributeNS(name.space, name.local);
}
function attrConstant() {
this.setAttribute(name, value);
}
function attrConstantNS() {
this.setAttributeNS(name.space, name.local, value);
}
function attrFunction() {
var x = value.apply(this, arguments);
if (x == null) this.removeAttribute(name); else this.setAttribute(name, x);
}
function attrFunctionNS() {
var x = value.apply(this, arguments);
if (x == null) this.removeAttributeNS(name.space, name.local); else this.setAttributeNS(name.space, name.local, x);
}
return value == null ? name.local ? attrNullNS : attrNull : typeof value === "function" ? name.local ? attrFunctionNS : attrFunction : name.local ? attrConstantNS : attrConstant;
}
d3_selectionPrototype.classed = function(name, value) {
if (arguments.length < 2) {
if (typeof name === "string") {
var node = this.node(), n = (name = name.trim().split(/^|\s+/g)).length, i = -1;
if (value = node.classList) {
while (++i < n) if (!value.contains(name[i])) return false;
} else {
value = node.getAttribute("class");
while (++i < n) if (!d3_selection_classedRe(name[i]).test(value)) return false;
}
return true;
}
for (value in name) this.each(d3_selection_classed(value, name[value]));
return this;
}
return this.each(d3_selection_classed(name, value));
};
function d3_selection_classedRe(name) {
return new RegExp("(?:^|\\s+)" + d3.requote(name) + "(?:\\s+|$)", "g");
}
function d3_selection_classed(name, value) {
name = name.trim().split(/\s+/).map(d3_selection_classedName);
var n = name.length;
function classedConstant() {
var i = -1;
while (++i < n) name[i](this, value);
}
function classedFunction() {
var i = -1, x = value.apply(this, arguments);
while (++i < n) name[i](this, x);
}
return typeof value === "function" ? classedFunction : classedConstant;
}
function d3_selection_classedName(name) {
var re = d3_selection_classedRe(name);
return function(node, value) {
if (c = node.classList) return value ? c.add(name) : c.remove(name);
var c = node.getAttribute("class") || "";
if (value) {
re.lastIndex = 0;
if (!re.test(c)) node.setAttribute("class", d3_collapse(c + " " + name));
} else {
node.setAttribute("class", d3_collapse(c.replace(re, " ")));
}
};
}
d3_selectionPrototype.style = function(name, value, priority) {
var n = arguments.length;
if (n < 3) {
if (typeof name !== "string") {
if (n < 2) value = "";
for (priority in name) this.each(d3_selection_style(priority, name[priority], value));
return this;
}
if (n < 2) return d3_window.getComputedStyle(this.node(), null).getPropertyValue(name);
priority = "";
}
return this.each(d3_selection_style(name, value, priority));
};
function d3_selection_style(name, value, priority) {
function styleNull() {
this.style.removeProperty(name);
}
function styleConstant() {
this.style.setProperty(name, value, priority);
}
function styleFunction() {
var x = value.apply(this, arguments);
if (x == null) this.style.removeProperty(name); else this.style.setProperty(name, x, priority);
}
return value == null ? styleNull : typeof value === "function" ? styleFunction : styleConstant;
}
d3_selectionPrototype.property = function(name, value) {
if (arguments.length < 2) {
if (typeof name === "string") return this.node()[name];
for (value in name) this.each(d3_selection_property(value, name[value]));
return this;
}
return this.each(d3_selection_property(name, value));
};
function d3_selection_property(name, value) {
function propertyNull() {
delete this[name];
}
function propertyConstant() {
this[name] = value;
}
function propertyFunction() {
var x = value.apply(this, arguments);
if (x == null) delete this[name]; else this[name] = x;
}
return value == null ? propertyNull : typeof value === "function" ? propertyFunction : propertyConstant;
}
d3_selectionPrototype.text = function(value) {
return arguments.length ? this.each(typeof value === "function" ? function() {
var v = value.apply(this, arguments);
this.textContent = v == null ? "" : v;
} : value == null ? function() {
this.textContent = "";
} : function() {
this.textContent = value;
}) : this.node().textContent;
};
d3_selectionPrototype.html = function(value) {
return arguments.length ? this.each(typeof value === "function" ? function() {
var v = value.apply(this, arguments);
this.innerHTML = v == null ? "" : v;
} : value == null ? function() {
this.innerHTML = "";
} : function() {
this.innerHTML = value;
}) : this.node().innerHTML;
};
d3_selectionPrototype.append = function(name) {
name = d3_selection_creator(name);
return this.select(function() {
return this.appendChild(name.apply(this, arguments));
});
};
function d3_selection_creator(name) {
return typeof name === "function" ? name : (name = d3.ns.qualify(name)).local ? function() {
return this.ownerDocument.createElementNS(name.space, name.local);
} : function() {
return this.ownerDocument.createElementNS(this.namespaceURI, name);
};
}
d3_selectionPrototype.insert = function(name, before) {
name = d3_selection_creator(name);
before = d3_selection_selector(before);
return this.select(function() {
return this.insertBefore(name.apply(this, arguments), before.apply(this, arguments) || null);
});
};
d3_selectionPrototype.remove = function() {
return this.each(function() {
var parent = this.parentNode;
if (parent) parent.removeChild(this);
});
};
d3_selectionPrototype.data = function(value, key) {
var i = -1, n = this.length, group, node;
if (!arguments.length) {
value = new Array(n = (group = this[0]).length);
while (++i < n) {
if (node = group[i]) {
value[i] = node.__data__;
}
}
return value;
}
function bind(group, groupData) {
var i, n = group.length, m = groupData.length, n0 = Math.min(n, m), updateNodes = new Array(m), enterNodes = new Array(m), exitNodes = new Array(n), node, nodeData;
if (key) {
var nodeByKeyValue = new d3_Map(), dataByKeyValue = new d3_Map(), keyValues = [], keyValue;
for (i = -1; ++i < n; ) {
keyValue = key.call(node = group[i], node.__data__, i);
if (nodeByKeyValue.has(keyValue)) {
exitNodes[i] = node;
} else {
nodeByKeyValue.set(keyValue, node);
}
keyValues.push(keyValue);
}
for (i = -1; ++i < m; ) {
keyValue = key.call(groupData, nodeData = groupData[i], i);
if (node = nodeByKeyValue.get(keyValue)) {
updateNodes[i] = node;
node.__data__ = nodeData;
} else if (!dataByKeyValue.has(keyValue)) {
enterNodes[i] = d3_selection_dataNode(nodeData);
}
dataByKeyValue.set(keyValue, nodeData);
nodeByKeyValue.remove(keyValue);
}
for (i = -1; ++i < n; ) {
if (nodeByKeyValue.has(keyValues[i])) {
exitNodes[i] = group[i];
}
}
} else {
for (i = -1; ++i < n0; ) {
node = group[i];
nodeData = groupData[i];
if (node) {
node.__data__ = nodeData;
updateNodes[i] = node;
} else {
enterNodes[i] = d3_selection_dataNode(nodeData);
}
}
for (;i < m; ++i) {
enterNodes[i] = d3_selection_dataNode(groupData[i]);
}
for (;i < n; ++i) {
exitNodes[i] = group[i];
}
}
enterNodes.update = updateNodes;
enterNodes.parentNode = updateNodes.parentNode = exitNodes.parentNode = group.parentNode;
enter.push(enterNodes);
update.push(updateNodes);
exit.push(exitNodes);
}
var enter = d3_selection_enter([]), update = d3_selection([]), exit = d3_selection([]);
if (typeof value === "function") {
while (++i < n) {
bind(group = this[i], value.call(group, group.parentNode.__data__, i));
}
} else {
while (++i < n) {
bind(group = this[i], value);
}
}
update.enter = function() {
return enter;
};
update.exit = function() {
return exit;
};
return update;
};
function d3_selection_dataNode(data) {
return {
__data__: data
};
}
d3_selectionPrototype.datum = function(value) {
return arguments.length ? this.property("__data__", value) : this.property("__data__");
};
d3_selectionPrototype.each = function(callback) {
return d3_selection_each(this, function(node, i, j) {
callback.call(node, node.__data__, i, j);
});
};
function d3_selection_each(groups, callback) {
for (var j = 0, m = groups.length; j < m; j++) {
for (var group = groups[j], i = 0, n = group.length, node; i < n; i++) {
if (node = group[i]) callback(node, i, j);
}
}
return groups;
}
d3_selectionPrototype.call = function(callback) {
var args = d3_array(arguments);
callback.apply(args[0] = this, args);
return this;
};
d3_selectionPrototype.empty = function() {
return !this.node();
};
d3_selectionPrototype.node = function() {
for (var j = 0, m = this.length; j < m; j++) {
for (var group = this[j], i = 0, n = group.length; i < n; i++) {
var node = group[i];
if (node) return node;
}
}
return null;
};
function d3_selection_enter(selection) {
d3_subclass(selection, d3_selection_enterPrototype);
return selection;
}
var d3_selection_enterPrototype = [];
d3.selection.enter = d3_selection_enter;
d3.selection.enter.prototype = d3_selection_enterPrototype;
d3_selection_enterPrototype.append = d3_selectionPrototype.append;
d3_selection_enterPrototype.empty = d3_selectionPrototype.empty;
d3_selection_enterPrototype.node = d3_selectionPrototype.node;
d3_selection_enterPrototype.call = d3_selectionPrototype.call;
d3_selection_enterPrototype.select = function(selector) {
var subgroups = [], subgroup, subnode, upgroup, group, node;
for (var j = -1, m = this.length; ++j < m; ) {
upgroup = (group = this[j]).update;
subgroups.push(subgroup = []);
subgroup.parentNode = group.parentNode;
for (var i = -1, n = group.length; ++i < n; ) {
if (node = group[i]) {
subgroup.push(upgroup[i] = subnode = selector.call(group.parentNode, node.__data__, i, j));
subnode.__data__ = node.__data__;
} else {
subgroup.push(null);
}
}
}
return d3_selection(subgroups);
};
d3_selection_enterPrototype.insert = function(name, before) {
if (arguments.length < 2) before = d3_selection_enterInsertBefore(this);
return d3_selectionPrototype.insert.call(this, name, before);
};
function d3_selection_enterInsertBefore(enter) {
var i0, j0;
return function(d, i, j) {
var group = enter[j].update, n = group.length, node;
if (j != j0) j0 = j, i0 = 0;
if (i >= i0) i0 = i + 1;
while (!(node = group[i0]) && ++i0 < n) ;
return node;
};
}
d3.select = function(node) {
var group = [ typeof node === "string" ? d3_select(node, d3_document) : node ];
group.parentNode = d3_documentElement;
return d3_selection([ group ]);
};
d3.selectAll = function(nodes) {
var group = d3_array(typeof nodes === "string" ? d3_selectAll(nodes, d3_document) : nodes);
group.parentNode = d3_documentElement;
return d3_selection([ group ]);
};
d3_selectionPrototype.on = function(type, listener, capture) {
var n = arguments.length;
if (n < 3) {
if (typeof type !== "string") {
if (n < 2) listener = false;
for (capture in type) this.each(d3_selection_on(capture, type[capture], listener));
return this;
}
if (n < 2) return (n = this.node()["__on" + type]) && n._;
capture = false;
}
return this.each(d3_selection_on(type, listener, capture));
};
function d3_selection_on(type, listener, capture) {
var name = "__on" + type, i = type.indexOf("."), wrap = d3_selection_onListener;
if (i > 0) type = type.substring(0, i);
var filter = d3_selection_onFilters.get(type);
if (filter) type = filter, wrap = d3_selection_onFilter;
function onRemove() {
var l = this[name];
if (l) {
this.removeEventListener(type, l, l.$);
delete this[name];
}
}
function onAdd() {
var l = wrap(listener, d3_array(arguments));
onRemove.call(this);
this.addEventListener(type, this[name] = l, l.$ = capture);
l._ = listener;
}
function removeAll() {
var re = new RegExp("^__on([^.]+)" + d3.requote(type) + "$"), match;
for (var name in this) {
if (match = name.match(re)) {
var l = this[name];
this.removeEventListener(match[1], l, l.$);
delete this[name];
}
}
}
return i ? listener ? onAdd : onRemove : listener ? d3_noop : removeAll;
}
var d3_selection_onFilters = d3.map({
mouseenter: "mouseover",
mouseleave: "mouseout"
});
d3_selection_onFilters.forEach(function(k) {
if ("on" + k in d3_document) d3_selection_onFilters.remove(k);
});
function d3_selection_onListener(listener, argumentz) {
return function(e) {
var o = d3.event;
d3.event = e;
argumentz[0] = this.__data__;
try {
listener.apply(this, argumentz);
} finally {
d3.event = o;
}
};
}
d3.mouse = function(container) {
return d3_mousePoint(container, d3_eventSource());
};
var d3_mouse_bug44083 = /WebKit/.test(d3_window.navigator.userAgent) ? -1 : 0;
function d3_mousePoint(container, e) {
if (e.changedTouches) e = e.changedTouches[0];
var svg = container.ownerSVGElement || container;
if (svg.createSVGPoint) {
var point = svg.createSVGPoint();
if (d3_mouse_bug44083 < 0 && (d3_window.scrollX || d3_window.scrollY)) {
svg = d3.select("body").append("svg").style({
position: "absolute",
top: 0,
left: 0,
margin: 0,
padding: 0,
border: "none"
}, "important");
var ctm = svg[0][0].getScreenCTM();
d3_mouse_bug44083 = !(ctm.f || ctm.e);
svg.remove();
}
if (d3_mouse_bug44083) point.x = e.pageX, point.y = e.pageY; else point.x = e.clientX,
point.y = e.clientY;
point = point.matrixTransform(container.getScreenCTM().inverse());
return [ point.x, point.y ];
}
var rect = container.getBoundingClientRect();
return [ e.clientX - rect.left - container.clientLeft, e.clientY - rect.top - container.clientTop ];
}
var π = Math.PI, τ = 2 * π, halfπ = π / 2, ε = 1e-6, ε2 = ε * ε, d3_radians = π / 180, d3_degrees = 180 / π;
function d3_functor(v) {
return typeof v === "function" ? v : function() {
return v;
};
}
d3.functor = d3_functor;
function d3_identity(d) {
return d;
}
var d3_format_decimalPoint = ".", d3_format_thousandsSeparator = ",", d3_format_grouping = [ 3, 3 ], d3_format_currencySymbol = "$";
function d3_formatPrefix(d, i) {
var k = Math.pow(10, abs(8 - i) * 3);
return {
scale: i > 8 ? function(d) {
return d / k;
} : function(d) {
return d * k;
},
symbol: d
};
}
d3.format = function(specifier) {
var match = d3_format_re.exec(specifier), fill = match[1] || " ", align = match[2] || ">", sign = match[3] || "", symbol = match[4] || "", zfill = match[5], width = +match[6], comma = match[7], precision = match[8], type = match[9], scale = 1, suffix = "", integer = false;
if (precision) precision = +precision.substring(1);
if (zfill || fill === "0" && align === "=") {
zfill = fill = "0";
align = "=";
if (comma) width -= Math.floor((width - 1) / 4);
}
switch (type) {
case "n":
comma = true;
type = "g";
break;
case "%":
scale = 100;
suffix = "%";
type = "f";
break;
case "p":
scale = 100;
suffix = "%";
type = "r";
break;
case "b":
case "o":
case "x":
case "X":
if (symbol === "#") symbol = "0" + type.toLowerCase();
case "c":
case "d":
integer = true;
precision = 0;
break;
case "s":
scale = -1;
type = "r";
break;
}
if (symbol === "#") symbol = ""; else if (symbol === "$") symbol = d3_format_currencySymbol;
if (type == "r" && !precision) type = "g";
if (precision != null) {
if (type == "g") precision = Math.max(1, Math.min(21, precision)); else if (type == "e" || type == "f") precision = Math.max(0, Math.min(20, precision));
}
type = d3_format_types.get(type) || d3_format_typeDefault;
var zcomma = zfill && comma;
return function(value) {
if (integer && value % 1) return "";
var negative = value < 0 || value === 0 && 1 / value < 0 ? (value = -value, "-") : sign;
if (scale < 0) {
var prefix = d3.formatPrefix(value, precision);
value = prefix.scale(value);
suffix = prefix.symbol;
} else {
value *= scale;
}
value = type(value, precision);
var i = value.lastIndexOf("."), before = i < 0 ? value : value.substring(0, i), after = i < 0 ? "" : d3_format_decimalPoint + value.substring(i + 1);
if (!zfill && comma) before = d3_format_group(before);
var length = symbol.length + before.length + after.length + (zcomma ? 0 : negative.length), padding = length < width ? new Array(length = width - length + 1).join(fill) : "";
if (zcomma) before = d3_format_group(padding + before);
negative += symbol;
value = before + after;
return (align === "<" ? negative + value + padding : align === ">" ? padding + negative + value : align === "^" ? padding.substring(0, length >>= 1) + negative + value + padding.substring(length) : negative + (zcomma ? value : padding + value)) + suffix;
};
};
var d3_format_re = /(?:([^{])?([<>=^]))?([+\- ])?([$#])?(0)?(\d+)?(,)?(\.-?\d+)?([a-z%])?/i;
var d3_format_types = d3.map({
f: function(x, p) {
return x.toFixed(p);
}
});
function d3_format_precision(x, p) {
return p - (x ? Math.ceil(Math.log(x) / Math.LN10) : 1);
}
function d3_format_typeDefault(x) {
return x + "";
}
var d3_format_group = d3_identity;
if (d3_format_grouping) {
var d3_format_groupingLength = d3_format_grouping.length;
d3_format_group = function(value) {
var i = value.length, t = [], j = 0, g = d3_format_grouping[0];
while (i > 0 && g > 0) {
t.push(value.substring(i -= g, i + g));
g = d3_format_grouping[j = (j + 1) % d3_format_groupingLength];
}
return t.reverse().join(d3_format_thousandsSeparator);
};
}
function d3_true() {
return true;
}
function d3_geom_pointX(d) {
return d[0];
}
function d3_geom_pointY(d) {
return d[1];
}
d3.interpolateNumber = d3_interpolateNumber;
function d3_interpolateNumber(a, b) {
b -= a = +a;
return function(t) {
return a + b * t;
};
}
var d3_interpolate_number = /[-+]?(?:\d+\.?\d*|\.?\d+)(?:[eE][-+]?\d+)?/g;
d3.interpolate = d3_interpolate;
function d3_interpolate(a, b) {
var i = d3.interpolators.length, f;
while (--i >= 0 && !(f = d3.interpolators[i](a, b))) ;
return f;
}
d3.interpolators = [ function(a, b) {
var t = typeof b;
return (t === "string" ? d3_rgb_names.has(b) || /^(#|rgb\(|hsl\()/.test(b) ? d3_interpolateRgb : d3_interpolateString : t === "object" ? Array.isArray(b) ? d3_interpolateArray : d3_interpolateObject : d3_interpolateNumber)(a, b);
} ];
function d3_uninterpolateNumber(a, b) {
b = b - (a = +a) ? 1 / (b - a) : 0;
return function(x) {
return (x - a) * b;
};
}
d3.scale = {};
function d3_scaleExtent(domain) {
var start = domain[0], stop = domain[domain.length - 1];
return start < stop ? [ start, stop ] : [ stop, start ];
}
function d3_scaleRange(scale) {
return scale.rangeExtent ? scale.rangeExtent() : d3_scaleExtent(scale.range());
}
function d3_scale_bilinear(domain, range, uninterpolate, interpolate) {
var u = uninterpolate(domain[0], domain[1]), i = interpolate(range[0], range[1]);
return function(x) {
return i(u(x));
};
}
d3.scale.linear = function() {
return d3_scale_linear([ 0, 1 ], [ 0, 1 ], d3_interpolate, false);
};
function d3_scale_linear(domain, range, interpolate, clamp) {
var output, input;
function rescale() {
var linear = Math.min(domain.length, range.length) > 2 ? d3_scale_polylinear : d3_scale_bilinear, uninterpolate = clamp ? d3_uninterpolateClamp : d3_uninterpolateNumber;
output = linear(domain, range, uninterpolate, interpolate);
input = linear(range, domain, uninterpolate, d3_interpolate);
return scale;
}
function scale(x) {
return output(x);
}
scale.invert = function(y) {
return input(y);
};
scale.range = function(x) {
if (!arguments.length) return range;
range = x;
return rescale();
};
scale.ticks = function(m) {
return d3_scale_linearTicks(domain, m);
};
scale.tickFormat = function(m, format) {
return d3_scale_linearTickFormat(domain, m, format);
};
scale.copy = function() {
return d3_scale_linear(domain, range, interpolate, clamp);
};
return rescale();
}
function d3_scale_linearTickRange(domain, m) {
if (m == null) m = 10;
var extent = d3_scaleExtent(domain), span = extent[1] - extent[0], step = Math.pow(10, Math.floor(Math.log(span / m) / Math.LN10)), err = m / span * step;
if (err <= .15) step *= 10; else if (err <= .35) step *= 5; else if (err <= .75) step *= 2;
extent[0] = Math.ceil(extent[0] / step) * step;
extent[1] = Math.floor(extent[1] / step) * step + step * .5;
extent[2] = step;
return extent;
}
function d3_scale_linearTicks(domain, m) {
return d3.range.apply(d3, d3_scale_linearTickRange(domain, m));
}
function d3_scale_linearTickFormat(domain, m, format) {
var range = d3_scale_linearTickRange(domain, m);
return d3.format(format ? format.replace(d3_format_re, function(a, b, c, d, e, f, g, h, i, j) {
return [ b, c, d, e, f, g, h, i || "." + d3_scale_linearFormatPrecision(j, range), j ].join("");
}) : ",." + d3_scale_linearPrecision(range[2]) + "f");
}
function d3_scale_linearPrecision(value) {
return -Math.floor(Math.log(value) / Math.LN10 + .01);
}
d3.svg = {};
function d3_svg_line(projection) {
var x = d3_geom_pointX, y = d3_geom_pointY, defined = d3_true, interpolate = d3_svg_lineLinear, interpolateKey = interpolate.key, tension = .7;
function line(data) {
var segments = [], points = [], i = -1, n = data.length, d, fx = d3_functor(x), fy = d3_functor(y);
function segment() {
segments.push("M", interpolate(projection(points), tension));
}
while (++i < n) {
if (defined.call(this, d = data[i], i)) {
points.push([ +fx.call(this, d, i), +fy.call(this, d, i) ]);
} else if (points.length) {
segment();
points = [];
}
}
if (points.length) segment();
return segments.length ? segments.join("") : null;
}
line.x = function(_) {
if (!arguments.length) return x;
x = _;
return line;
};
line.y = function(_) {
if (!arguments.length) return y;
y = _;
return line;
};
return line;
}
d3.svg.line = function() {
return d3_svg_line(d3_identity);
};
var d3_svg_lineInterpolators = d3.map({
"linear-closed": d3_svg_lineLinearClosed
});
d3_svg_lineInterpolators.forEach(function(key, value) {
value.key = key;
value.closed = /-closed$/.test(key);
});
function d3_svg_lineLinear(points) {
return points.join("L");
}
function d3_svg_lineLinearClosed(points) {
return d3_svg_lineLinear(points) + "Z";
}
d3.svg.axis = function() {
var scale = d3.scale.linear(), orient = d3_svg_axisDefaultOrient, innerTickSize = 6, outerTickSize = 6, tickPadding = 3, tickArguments_ = [ 10 ], tickValues = null, tickFormat_;
function axis(g) {
g.each(function() {
var g = d3.select(this);
var scale0 = this.__chart__ || scale, scale1 = this.__chart__ = scale.copy();
var ticks = tickValues == null ? scale1.ticks ? scale1.ticks.apply(scale1, tickArguments_) : scale1.domain() : tickValues, tickFormat = tickFormat_ == null ? scale1.tickFormat ? scale1.tickFormat.apply(scale1, tickArguments_) : d3_identity : tickFormat_, tick = g.selectAll(".tick").data(ticks, scale1), tickEnter = tick.enter().insert("g", ".domain").attr("class", "tick").style("opacity", ε), tickExit = tick.exit().style("opacity", ε).remove(), tickUpdate = tick.style("opacity", 1), tickTransform;
var range = d3_scaleRange(scale1), path = g.selectAll(".domain").data([ 0 ]), pathUpdate = (path.enter().append("path").attr("class", "domain"),
path);
tickEnter.append("line");
tickEnter.append("text");
var lineEnter = tickEnter.select("line"), lineUpdate = tickUpdate.select("line"), text = tick.select("text").text(tickFormat), textEnter = tickEnter.select("text"), textUpdate = tickUpdate.select("text");
switch (orient) {
case "bottom":
{
tickTransform = d3_svg_axisX;
lineEnter.attr("y2", innerTickSize);
textEnter.attr("y", Math.max(innerTickSize, 0) + tickPadding);
lineUpdate.attr("x2", 0).attr("y2", innerTickSize);
textUpdate.attr("x", 0).attr("y", Math.max(innerTickSize, 0) + tickPadding);
text.attr("dy", ".71em").style("text-anchor", "middle");
pathUpdate.attr("d", "M" + range[0] + "," + outerTickSize + "V0H" + range[1] + "V" + outerTickSize);
break;
}
case "top":
{
tickTransform = d3_svg_axisX;
lineEnter.attr("y2", -innerTickSize);
textEnter.attr("y", -(Math.max(innerTickSize, 0) + tickPadding));
lineUpdate.attr("x2", 0).attr("y2", -innerTickSize);
textUpdate.attr("x", 0).attr("y", -(Math.max(innerTickSize, 0) + tickPadding));
text.attr("dy", "0em").style("text-anchor", "middle");
pathUpdate.attr("d", "M" + range[0] + "," + -outerTickSize + "V0H" + range[1] + "V" + -outerTickSize);
break;
}
case "left":
{
tickTransform = d3_svg_axisY;
lineEnter.attr("x2", -innerTickSize);
textEnter.attr("x", -(Math.max(innerTickSize, 0) + tickPadding));
lineUpdate.attr("x2", -innerTickSize).attr("y2", 0);
textUpdate.attr("x", -(Math.max(innerTickSize, 0) + tickPadding)).attr("y", 0);
text.attr("dy", ".32em").style("text-anchor", "end");
pathUpdate.attr("d", "M" + -outerTickSize + "," + range[0] + "H0V" + range[1] + "H" + -outerTickSize);
break;
}
case "right":
{
tickTransform = d3_svg_axisY;
lineEnter.attr("x2", innerTickSize);
textEnter.attr("x", Math.max(innerTickSize, 0) + tickPadding);
lineUpdate.attr("x2", innerTickSize).attr("y2", 0);
textUpdate.attr("x", Math.max(innerTickSize, 0) + tickPadding).attr("y", 0);
text.attr("dy", ".32em").style("text-anchor", "start");
pathUpdate.attr("d", "M" + outerTickSize + "," + range[0] + "H0V" + range[1] + "H" + outerTickSize);
break;
}
}
if (scale1.rangeBand) {
var x = scale1, dx = x.rangeBand() / 2;
scale0 = scale1 = function(d) {
return x(d) + dx;
};
} else if (scale0.rangeBand) {
scale0 = scale1;
} else {
tickExit.call(tickTransform, scale1);
}
tickEnter.call(tickTransform, scale0);
tickUpdate.call(tickTransform, scale1);
});
}
axis.scale = function(x) {
if (!arguments.length) return scale;
scale = x;
return axis;
};
axis.orient = function(x) {
if (!arguments.length) return orient;
orient = x in d3_svg_axisOrients ? x + "" : d3_svg_axisDefaultOrient;
return axis;
};
axis.ticks = function() {
if (!arguments.length) return tickArguments_;
tickArguments_ = arguments;
return axis;
};
axis.tickFormat = function(x) {
if (!arguments.length) return tickFormat_;
tickFormat_ = x;
return axis;
};
axis.tickSize = function(x) {
var n = arguments.length;
if (!n) return innerTickSize;
innerTickSize = +x;
outerTickSize = +arguments[n - 1];
return axis;
};
return axis;
};
var d3_svg_axisDefaultOrient = "bottom", d3_svg_axisOrients = {
top: 1,
right: 1,
bottom: 1,
left: 1
};
function d3_svg_axisX(selection, x) {
selection.attr("transform", function(d) {
return "translate(" + x(d) + ",0)";
});
}
function d3_svg_axisY(selection, y) {
selection.attr("transform", function(d) {
return "translate(0," + y(d) + ")";
});
}
return d3;
}();
</script>
<script>
/*** Model ***/
var model = window;
// Select initial values based on rain example.
var prior = 0.07;
var sensitivity = 0.80;
var specificity = 0.95;
function isClassificationContradition() {
return sensitivity === 0 && specificity === 1;
}
function bayes(prior, sensitivity, specificity) {
if (prior === 0 || prior === 1 || // extreme prior
sensitivity === 0 && specificity === 1) { // contradiction
return prior;
}
return sensitivity * prior /
(sensitivity * prior + (1 - specificity) * (1 - prior));
}
function bayesAt(variable, value) {
switch (variable) {
case "prior":
return bayes(value, sensitivity, specificity);
case "sensitivity":
return bayes(prior, value, specificity);
case "specificity":
return bayes(prior, sensitivity, value);
default:
throw new TypeError("invalid variable "+variable);
}
}
var posteriorFindTolerance = 0.00001;
function withinTolerance(v, w) {
return Math.abs(v - w) < posteriorFindTolerance;
}
function bayesFind(variable, posteriorValue) {
// Use binary search.
var min = 0;
var max = 1;
for (;;) {
var middle = (max + min) / 2;
if (withinTolerance(middle, 0)) {
return 0;
}
if (withinTolerance(middle, 1)) {
return 1;
}
var v = bayesAt(variable, middle);
if (withinTolerance(v, posteriorValue)) {
return middle;
}
if (v < posteriorValue) {
min = middle;
} else {
max = middle;
}
}
}
function between(x, lower, upper) {
return lower <= x && x <= upper;
}
function square(x) {
return x * x;
}
function clamp(value) {
return Math.max(0, Math.min(value, 1));
}
Object.defineProperty(model, "fScore", {
get: function() {
var sum = sensitivity + specificity;
return sum === 0 ? 0 : 2 * sensitivity * specificity / sum;
}
});
Object.defineProperty(model, "posterior", {
get: function() {
return bayes(prior, sensitivity, specificity);
},
set: function(value) {
// Use hill climbing.
var maxIterations = 100000;
var samples = 1000;
var wiggliness = 100;
var posteriorWeight = 1000000;
var basePrior = prior;
var baseSensitivity = sensitivity;
var baseSpecificity = specificity;
function objective(prior, sensitivity, specificity) {
var posterior = bayes(prior, sensitivity, specificity);
return square(posterior - value) * posteriorWeight +
square(prior - basePrior) +
square(sensitivity - baseSensitivity) +
square(specificity - baseSpecificity);
}
function wiggle(v) {
return clamp(v + wiggliness * posteriorFindTolerance *
(Math.random() - 0.5));
}
for (var i = 0; i < maxIterations; ++i) {
if (withinTolerance(posterior, value)) {
break;
}
var bestPrior = prior;
var bestSensitivity = sensitivity;
var bestSpecificity = specificity;
var bestValue = objective(prior, sensitivity, specificity);
for (var j = 0; j < samples; ++j) {
// Wiggle independent variables.
var p = wiggle(prior);
var sn = wiggle(sensitivity);
var sp = wiggle(specificity);
// Choose the better.
var newValue = objective(p, sn, sp);
if (newValue < bestValue) {
bestValue = newValue;
bestPrior = p;
bestSensitivity = sn;
bestSpecificity = sp;
}
}
prior = bestPrior;
sensitivity = bestSensitivity;
specificity = bestSpecificity;
}
}
});
// extraPush helps posterior round toward amount.
function nudge(variable, amount, extraPush) {
if (extraPush === undefined) {
extraPush = 0;
}
var value = model[variable];
var clamped = Math.floor(value / amount) * amount;
model[variable] = clamp(clamped + amount + extraPush);
}
/*** Perspective ***/
var selectedVariable = "prior"; // one of the independent variables.
var selectedPosterior = false; // a separate selection.
var controlState = "inert"; // one of "inert", "focused", "active".
var explicitFocusValue = null;
var selectedGuideText = false;
var dynamicSelections = [];
function dynamic(selection, redrawFn) {
if (typeof redrawFn == "undefined") {
redrawFn = selection;
selection = null;
}
dynamicSelections.push({
selection: selection,
redraw: redrawFn
});
redrawFn(selection);
return selection;
}
var redrawRequested = null;
function redraw() {
if (redrawRequested === null) {
redrawRequested = requestAnimationFrame(redrawNow);
}
}
function redrawNow() {
for (var i = 0; i < dynamicSelections.length; ++i) {
dynamicSelections[i].redraw(dynamicSelections[i].selection);
}
redrawRequested = null;
}
function transition(states, d, i) {
var transitionFn = states[controlState];
if (typeof transitionFn !== "function") {
return;
}
return transitionFn(d, i);
}
function on(selection, type, listener) {
if (typeof listener !== "function") {
var states = listener;
listener = function(d, i) {
return transition(states, d, i);
}
}
return selection.on(type, function(d, i) {
var newState = listener(d, i);
if (typeof newState == "string") {
controlState = newState;
}
redraw();
});
}
/*** View ***/
var width = 400;
var height = 400;
var margin = {
top: 20, // just for spacing.
right: 40, // for overflow of xGuideText.
bottom: 120, // for prior, sensitivity, and specificity sliders.
left: 100, // for the posterior slider.
};
var x = d3.scale.linear().range([0, width]);
var y = d3.scale.linear().range([height, 0]);
function xAxis() {
return d3.svg.axis()
.scale(x)
.orient("bottom")
.ticks(10);
}
function yAxis() {
return d3.svg.axis()
.scale(y)
.orient("left")
.ticks(10);
}
var svg = d3.select("main").append("svg")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom)
var svgg = svg.append("g")
.attr("transform", "translate("+margin.left+","+margin.top+")");
/** Grid **/
// Draw the x grid lines.
svgg.append("g")
.classed("grid", true)
.attr("transform", "translate(0,"+height+")")
.call(xAxis()
.tickSize(-height, 0, 0)
.tickFormat(""))
// Draw the y grid lines.
svgg.append("g")
.classed("grid", true)
.call(yAxis()
.tickSize(-width, 0, 0)
.tickFormat(""));
svgg.append("g")
.classed("axis", true)
.attr("transform", "translate(0," + height + ")")
.call(xAxis());
svgg.append("g")
.classed("axis", true)
.call(yAxis());
/** Guide **/
var xLabelOffsetBelowXAxis = 36;
var yLabelOffsetLeftOfYAxis = 60;
var labelOffsetFromLine = 3;
var guideVariable;
var guideValue;
var guideRangeValue;
var guideLineStarts = {};
dynamic(function() {
guideVariable = controlState === "inert" ?
null : selectedVariable;
guideValue = controlState === "active" ||
explicitFocusValue === null ?
model[selectedVariable] : explicitFocusValue;
guideRangeValue = bayesAt(selectedVariable, guideValue);
});
var guide = svgg.append("g")
.classed("guide", true);
function ifGuideShouldAppear(selection, updates) {
if (guideVariable !== null || selectedGuideText) {
selection.attr("display", null)
.call(updates);
} else {
selection.attr("display", "none");
}
}
var verticalGuideLine = guide.append("line")
.call(dynamic, function(s) {s
.call(ifGuideShouldAppear, function(s) {s
.attr("x1", x(guideValue))
.attr("x2", x(guideValue))
.attr("y1", guideLineStarts[selectedVariable])
.attr("y2", y(guideRangeValue));
});
});
var horizontalGuideLine = guide.append("line")
.call(dynamic, function(s) {s
.call(ifGuideShouldAppear, function(s) {s
.attr("x1", guideLineStarts["posterior"])
.attr("x2", x(guideValue))
.attr("y1", y(guideRangeValue))
.attr("y2", y(guideRangeValue));
});
});
var guideFormat = d3.format(".3f");
var xGuideText = guide.append("text")
.attr("y", y(0) + xLabelOffsetBelowXAxis)
.attr("dx", labelOffsetFromLine)
.call(dynamic, function(s) {s
.call(ifGuideShouldAppear, function(s) {s
.attr("x", x(guideValue))
.text(guideFormat(guideValue));
});
});
var yGuideText = guide.append("text")
.attr("x", x(0) - yLabelOffsetLeftOfYAxis)
.attr("dy", -labelOffsetFromLine)
.call(dynamic, function(s) {s
.call(ifGuideShouldAppear, function(s) {s
.attr("y", y(guideRangeValue))
.text(guideFormat(guideRangeValue));
});
});
dynamic(function() {
if (!selectedGuideText) {
return;
}
d3.select("body").classed("disable-user-select", false);
var node = (selectedPosterior ? yGuideText : xGuideText).node();
range = document.createRange();
// range.selectNode often selects too much.
range.setStart(node, 0);
range.setEnd(node, 1);
selection = window.getSelection();
selection.removeAllRanges();
selection.addRange(range);
});
/** Series **/
var seriesResolution = 200;
var seriesPoints = [];
for (var i = 0; i <= seriesResolution; ++i) {
seriesPoints.push(i / seriesResolution);
}
// Separate groups to ensure that circles are on top of all paths.
var seriesPaths = svgg.append("g")
.classed("series", true);
var seriesCircles = svgg.append("g")
.classed("series", true);
function series(variable) {
var line = d3.svg.line()
.x(x)
.y(function(d) { return y(bayesAt(variable, d)); });
seriesPaths.append("path")
.classed(variable, true)
.datum(variable)
.call(dynamic, function(s) {s
.attr("d", line(seriesPoints));
});
seriesCircles.append("circle")
.classed(variable, true)
.datum(variable)
.attr("r", 5)
.call(dynamic, function(s) {s
.attr("cx", x(model[variable]))
.attr("cy", y(posterior));
});
}
series("specificity");
series("sensitivity");
series("prior");
/** Slider Thumb Shadow **/
var sliderThumbShadow = svgg.append("filter")
.attr("id", "slider-thumb-shadow")
// Tell SVG how big the filter might be.
.attr("x", "-50%")
.attr("y", "-50%")
.attr("width", "200%")
.attr("height", "200%");
sliderThumbShadow.append("feGaussianBlur")
.attr("in", "SourceAlpha")
.attr("stdDeviation", 1.1)
.attr("result", "blur");
sliderThumbShadow.append("feOffset")
.attr("in", "blur")
.attr("dy", "1")
.attr("result", "shadow");
var shadowMerge = sliderThumbShadow.append("feMerge");
shadowMerge.append("feMergeNode").attr("in", "shadow");
shadowMerge.append("feMergeNode").attr("in", "SourceGraphic");
/** Slider **/
var thumbMajor = 10;
var thumbMinor = 16;
var hitZoneExtension = 20;
var sliderXProperties = {
v: "x", v1: "x1", v2: "x2",
scale: x, dimension: "width",
r: "rx", c: "cx"
};
var sliderYProperties = {
v: "y", v1: "y1", v2: "y2",
scale: y, dimension: "height",
r: "ry", c: "cy"
};
function slider(variable, offset, orientation) {
if (orientation === undefined) {
orientation = "horizontal";
}
switch (orientation) {
case "horizontal":
var major = sliderXProperties;
var minor = sliderYProperties;
break;
case "vertical":
var major = sliderYProperties;
var minor = sliderXProperties;
break;
default:
throw new TypeError("invalid orientation "+orientation);
}
var slider = svgg.append("g")
.classed("slider", true)
.classed(variable, true)
.datum(variable);
slider.append("line")
.classed("track", true)
.classed("maximum", true)
.attr(major.v1, major.scale(0))
.attr(major.v2, major.scale(1))
.attr(minor.v1, offset)
.attr(minor.v2, offset);
slider.append("line")
.classed("track", true)
.classed("minimum", true)
.attr(major.v1, major.scale(0))
.attr(minor.v1, offset)
.attr(minor.v2, offset)
.call(dynamic, function(s) {s
.attr(major.v2, major.scale(model[variable]));
});
slider.append("rect")
.classed("hit-zone", true)
.attr(major.v,
Math.min(major.scale(0), major.scale(1)) - hitZoneExtension)
.attr(major.dimension,
Math.abs(major.scale(1) - major.scale(0)) + 2 * hitZoneExtension)
.attr(minor.v, offset - thumbMinor / 2)
.attr(minor.dimension, thumbMinor);
slider.append("ellipse")
.classed("thumb", true)
.attr(major.r, thumbMajor / 2)
.attr(minor.r, thumbMinor / 2)
.attr("filter", "url(#slider-thumb-shadow)")
.attr(minor.c, offset)
.call(dynamic, function(s) {s
.attr(major.c, major.scale(model[variable]))
.style("stroke", function(d) {
return selectedVariable === d ? "black" : null;
});
});
guideLineStarts[variable] = offset;
}
var sliderSpacing = 24;
var firstSliderY = 445;
slider("prior", firstSliderY);
slider("sensitivity", firstSliderY + sliderSpacing);
slider("specificity", firstSliderY + sliderSpacing * 2);
slider("posterior", -70, "vertical")
/** Cursor **/
d3.selectAll("main svg, body")
.call(dynamic, function(s) {s
.style("cursor", controlState === "inert" ? null :
selectedPosterior ? "row-resize" : "col-resize");
});
// Don't want to select axis labels while dragging.
d3.select("body")
.call(dynamic, function(s) {s
.classed("disable-user-select",
controlState !== "inert" && !selectedGuideText);
});
/*** Controller ***/
function mousePositionValue(scale) {
if (scale === undefined) {
scale = x;
}
var m = d3.mouse(svgg.node());
return clamp(scale.invert(m[scale === x ? 0 : 1]));
}
// To prevent moveover and mousemove from interfering with each other.
var ignoreNextMouseMove = false;
function focusOn(variable) {
if (variable === "posterior") {
selectedPosterior = true;
} else {
selectedPosterior = false;
selectedVariable = variable;
}
ignoreNextMouseMove = true;
explicitFocusValue = null;
return "focused";
}
function focusOnWithValueFromMousePosition(variable) {
focusOn(variable);
explicitFocusValue = selectedPosterior ?
bayesFind(selectedVariable, mousePositionValue(y)) :
mousePositionValue();
return "focused";
}
if (svg.node().checkIntersection === undefined) {
function overlaps(t1, t2, x1, x2) {
return t1 < x1 ? t2 >= x1 : t1 <= x2;
}
// For Firefox, hack an adequate alternative.
svg.node().checkIntersection = function(element, rect) {
var left = rect.x - margin.left;
var right = left + rect.width;
var top = rect.y - margin.top;
var bottom = top + rect.height;
if (element === horizontalGuideLine.node()) {
var y = +horizontalGuideLine.attr("y1");
var x1 = +horizontalGuideLine.attr("x1");
var x2 = +horizontalGuideLine.attr("x2");
return between(y, top, bottom) &&
overlaps(left, right, x1, x2);
} else {
var x = +verticalGuideLine.attr("x1");
// Index reversed since we draw stroke upwards.
var y1 = +verticalGuideLine.attr("y2");
var y2 = +verticalGuideLine.attr("y1");
return between(x, left, right) &&
overlaps(top, bottom, y1, y2);
}
}
}
var isNearThreshhold = 20;
var isNearRect = svg.node().createSVGRect();
isNearRect.width = isNearRect.height = 2 * isNearThreshhold;
function isNear(selection) {
var m = d3.mouse(svg.node());
isNearRect.x = m[0] - isNearThreshhold;
isNearRect.y = m[1] - isNearThreshhold;
return svg.node().checkIntersection(selection.node(), isNearRect);
}
function movedAway(selection) {
return !isNear(selection);
}
function becomeFocusedIfIsNearGuideLines() {
return (isNear(verticalGuideLine) ||
isNear(horizontalGuideLine)) && "focused";
}
function becomeInertIfMovedAwayFromGuideLines() {
if (isNear(verticalGuideLine)) {
selectedPosterior = false;
} else if (isNear(horizontalGuideLine)) {
selectedPosterior = true;
}
return movedAway(verticalGuideLine) &&
movedAway(horizontalGuideLine) && "inert";
}
function updateSelectedVariableFromMousePosition() {
if (selectedPosterior) {
posterior = mousePositionValue(y);
} else {
model[selectedVariable] = mousePositionValue();
}
}
function activate(variable) {
if (variable === "posterior") {
selectedPosterior = true;
} else if (controlState !== "focused" && variable !== undefined) {
selectedVariable = variable;
}
explicitFocusValue = null;
selectedGuideText = false;
updateSelectedVariableFromMousePosition();
return "active";
}
svgg.selectAll(".series circle, .slider .thumb")
.call(on, "mouseover", {
inert: focusOn,
focused: focusOn
})
.call(on, "dblclick", function(variable) {
selectedGuideText = true;
});
svgg.selectAll(".series path, .slider .hit-zone")
.call(on, "mousemove", {
inert: focusOnWithValueFromMousePosition,
focused: focusOnWithValueFromMousePosition
})
.call(on, "mousedown", activate);
d3.select("main")
.call(on, "mousemove", function() {
if (ignoreNextMouseMove) {
ignoreNextMouseMove = false;
return;
}
return transition({
inert: becomeFocusedIfIsNearGuideLines,
focused: becomeInertIfMovedAwayFromGuideLines,
active: updateSelectedVariableFromMousePosition
});
})
.call(on, "mousedown", {
inert: function() {
selectedGuideText = false;
},
focused: activate
})
.call(on, "mouseup", {
active: d3.functor("focused")
});
d3.select("body")
.call(on, "keydown", function() {
switch (d3.event.keyCode) {
case 37: // left
nudge(selectedVariable, -0.001);
break;
case 39: // right
nudge(selectedVariable, 0.001);
break;
case 38: // up
nudge("posterior", 0.001, posteriorFindTolerance);
break;
case 40: // down
nudge("posterior", -0.001, -posteriorFindTolerance);
break;
default:
return;
}
controlState = "focused";
explicitFocusValue = null;
d3.event.preventDefault(); // to avoid scrolling.
});
</script>
<div id="details">
<div id="example">
<h3>Example:</h3>
<div class="switch-extreme-prior">
<div class="case normal">
<p>Imagine there is generally
<span class="a-an-prior">{a|an}</span>
<span class="percent prior">%%</span> chance <br>
of rain on any given day where you live. Siri says, &ldquo;It looks like there will be some rain today.&rdquo;</p>
<p>Siri
<span class="switch-f-score">
<span class="case lies">lies:</span>
<span class="case tea">would be better off reading tea leaves:</span>
<span class="case sources">could use some better sources:</span>
<span class="case majored">majored in meteorology:</span>
<span class="case makes">makes the rain:</span>
</span>
</p>
<div class="switch-classification-contradition">
<div class="case true">
<ul>
<li>When it rains,
<div>Siri <span class="sensitivity">never</span> predicts rain.</div></li>
<li>When it doesn't rain,
<div>Siri <span class="specificity">always</span> predicts no rain.</div></li>
</ul>
<p>That's weird.</p>
<ul>
<li>Siri predicts rain,
<div>so then it <span class="sensitivity">won't</span> rain.</div>
</li>
<li>But with no rain,
<div>Siri <span class="specificity">wouldn't</span> have predicted rain in the first place.</div>
</li>
</ul>
</div>
<div class="case false">
<ul>
<li>When it rains,
<div>Siri predicts rain
<span class="percent sensitivity">%%</span> of the time.</div></li>
<li>When it doesn't rain,
<div>Siri predicts no rain
<span class="percent specificity">%%</span> of the time.</div></li>
</ul>
</div>
</div>
<p>
<span class="switch-classification-contradition">
<span class="case true">
Ignore Siri. Conclude that there's still
</span>
<span class="case false">
By Bayes rule, you should conclude that there's
</span>
</span>
<span class="a-an-posterior">{a|an}</span>
<span class="percent posterior">%%</span> chance of rain today.
<span class="switch-outlook">
<span class="case sun">&#x2600;</span>
<span class="case cloud">&#x2601;</span>
<span class="case umbrella">&#x2602;</span>
</span>
</p>
</div>
<div class="case zero">
<p>Suppose you believe there is <span class="prior">absolutely no chance</span> that it will rain where you live.</p>
<p>Then no report will convince you otherwise, and you likely live in LA.</p>
</div>
<div class="case one">
<p>Suppose you are <span class="prior">certain</span> it rains every day where you live.</p>
<p>Then no report will convince you otherwise, and you may be living in the Matrix.</p>
</div>
</div>
</div>
<script>
/** Example **/
var percentFormat = d3.format("%");
function formatExampleVariable(variable) {
d3.selectAll("#example .percent."+variable)
.call(dynamic, function(s) {s
.text(percentFormat(model[variable]));
});
d3.selectAll("#example .a-an-"+variable)
.call(dynamic, function(s) {
var v = Math.round(model[variable] * 100);
s.text(v === 8 || v === 11 || v === 18 || between(v, 80, 89)
? "an" : "a");
});
}
formatExampleVariable("prior");
formatExampleVariable("posterior");
formatExampleVariable("sensitivity");
formatExampleVariable("specificity");
d3.select("#example")
.call(dynamic, function(s) {s
.selectAll(".case").style("display", "none");
});
function displayExampleSwitch(switchName, toShow) {
var switchSelector = "#example .switch-"+switchName;
dynamic(function(s) {
var c = d3.selectAll(switchSelector+" > ."+toShow());
if (c.empty()) {
c = d3.selectAll(switchSelector+" > .default");
}
c.style("display", null);
});
}
displayExampleSwitch("f-score", function() {
return isClassificationContradition() ? "lies" :
posterior < prior ? "tea" :
fScore < 0.9 ? "sources" :
fScore < 1 ? "majored" :
"makes";
});
displayExampleSwitch("outlook", function() {
return posterior < 0.1 ? "sun" :
posterior < 0.7 ? "cloud" :
"umbrella"
});
displayExampleSwitch("extreme-prior", function() {
return prior === 0 ? "zero" :
prior === 1 ? "one" :
"normal";
});
displayExampleSwitch("classification-contradition",
isClassificationContradition);
</script>
<footer><address>
<div id="credits">
<div><span>
William Taysom 2014
</span>
<a class="license" rel="license" href="http://creativecommons.org/licenses/by-nc-sa/4.0/"><img alt="Creative Commons License" style="border-width:0; vertical-align: top" src="https://i.creativecommons.org/l/by-nc-sa/4.0/80x15.png" width="80" height="15"/></a></div>
<div>with thanks to
<a href="http://www.tlhiv.org/ltxpreview/">LaTeX Previewer</a> and
<a href="http://d3js.org/">d3</a>.</div>
</address></footer>
</div>
</main>
Display the source blob
Display the rendered blob
Raw
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
/** Example **/
var percentFormat = d3.format("%");
function formatExampleVariable(variable) {
d3.selectAll("#example .percent."+variable)
.call(dynamic, function(s) {s
.text(percentFormat(model[variable]));
});
d3.selectAll("#example .a-an-"+variable)
.call(dynamic, function(s) {
var v = Math.round(model[variable] * 100);
s.text(v === 8 || v === 11 || v === 18 || between(v, 80, 89)
? "an" : "a");
});
}
formatExampleVariable("prior");
formatExampleVariable("posterior");
formatExampleVariable("sensitivity");
formatExampleVariable("specificity");
d3.select("#example")
.call(dynamic, function(s) {s
.selectAll(".case").style("display", "none");
});
function displayExampleSwitch(switchName, toShow) {
var switchSelector = "#example .switch-"+switchName;
dynamic(function(s) {
var c = d3.selectAll(switchSelector+" > ."+toShow());
if (c.empty()) {
c = d3.selectAll(switchSelector+" > .default");
}
c.style("display", null);
});
}
displayExampleSwitch("f-score", function() {
return isClassificationContradition() ? "lies" :
posterior < prior ? "tea" :
fScore < 0.9 ? "sources" :
fScore < 1 ? "majored" :
"makes";
});
displayExampleSwitch("outlook", function() {
return posterior < 0.1 ? "sun" :
posterior < 0.7 ? "cloud" :
"umbrella"
});
displayExampleSwitch("extreme-prior", function() {
return prior === 0 ? "zero" :
prior === 1 ? "one" :
"normal";
});
displayExampleSwitch("classification-contradition",
isClassificationContradition);
<!DOCTYPE html>
<meta charset="utf-8">
<title>Bayes Rule</title>
<link rel="stylesheet" type="text/css" href="style.css">
<main>
<script src="http://d3js.org/d3.v3.min.js"></script>
<script src="plot.js"></script>
<div id="details">
<div id="example">
<h3>Example:</h3>
<div class="switch-extreme-prior">
<div class="case normal">
<p>Imagine there is generally
<span class="a-an-prior">{a|an}</span>
<span class="percent prior">%%</span> chance <br>
of rain on any given day where you live. Siri says, &ldquo;It looks like there will be some rain today.&rdquo;</p>
<p>Siri
<span class="switch-f-score">
<span class="case lies">lies:</span>
<span class="case tea">would be better off reading tea leaves:</span>
<span class="case sources">could use some better sources:</span>
<span class="case majored">majored in meteorology:</span>
<span class="case makes">makes the rain:</span>
</span>
</p>
<div class="switch-classification-contradition">
<div class="case true">
<ul>
<li>When it rains,
<div>Siri <span class="sensitivity">never</span> predicts rain.</div></li>
<li>When it doesn't rain,
<div>Siri <span class="specificity">always</span> predicts no rain.</div></li>
</ul>
<p>That's weird.</p>
<ul>
<li>Siri predicts rain,
<div>so then it <span class="sensitivity">won't</span> rain.</div>
</li>
<li>But with no rain,
<div>Siri <span class="specificity">wouldn't</span> have predicted rain in the first place.</div>
</li>
</ul>
</div>
<div class="case false">
<ul>
<li>When it rains,
<div>Siri predicts rain
<span class="percent sensitivity">%%</span> of the time.</div></li>
<li>When it doesn't rain,
<div>Siri predicts no rain
<span class="percent specificity">%%</span> of the time.</div></li>
</ul>
</div>
</div>
<p>
<span class="switch-classification-contradition">
<span class="case true">
Ignore Siri. Conclude that there's still
</span>
<span class="case false">
By Bayes rule, you should conclude that there's
</span>
</span>
<span class="a-an-posterior">{a|an}</span>
<span class="percent posterior">%%</span> chance of rain today.
<span class="switch-outlook">
<span class="case sun">&#x2600;</span>
<span class="case cloud">&#x2601;</span>
<span class="case umbrella">&#x2602;</span>
</span>
</p>
</div>
<div class="case zero">
<p>Suppose you believe there is <span class="prior">absolutely no chance</span> that it will rain where you live.</p>
<p>Then no report will convince you otherwise, and you likely live in LA.</p>
</div>
<div class="case one">
<p>Suppose you are <span class="prior">certain</span> it rains every day where you live.</p>
<p>Then no report will convince you otherwise, and you may be living in the Matrix.</p>
</div>
</div>
</div>
<script src="example.js"></script>
<footer><address><div id="credits">
<div><span>
William Taysom 2014
</span>
<a class="license" rel="license" href="http://creativecommons.org/licenses/by-nc-sa/4.0/"><img alt="Creative Commons License" style="border-width:0; vertical-align: top" src="https://i.creativecommons.org/l/by-nc-sa/4.0/80x15.png" width="80" height="15"/></a></div>
<div>with thanks to
<a href="http://www.tlhiv.org/ltxpreview/">LaTeX Previewer</a> and
<a href="http://d3js.org/">d3</a>.</div>
</div>
<div>Find single page version <a href="https://gist.githubusercontent.com/wtaysom/6f7ee490ec7642de6217/raw/bayes.html">here</a>. (Open it in a new tab.)</div>
</address></footer>
</div>
</main>
/*** Model ***/
var model = window;
// Select initial values based on rain example.
var prior = 0.07;
var sensitivity = 0.80;
var specificity = 0.95;
function isClassificationContradition() {
return sensitivity === 0 && specificity === 1;
}
function bayes(prior, sensitivity, specificity) {
if (prior === 0 || prior === 1 || // extreme prior
sensitivity === 0 && specificity === 1) { // contradiction
return prior;
}
return sensitivity * prior /
(sensitivity * prior + (1 - specificity) * (1 - prior));
}
function bayesAt(variable, value) {
switch (variable) {
case "prior":
return bayes(value, sensitivity, specificity);
case "sensitivity":
return bayes(prior, value, specificity);
case "specificity":
return bayes(prior, sensitivity, value);
default:
throw new TypeError("invalid variable "+variable);
}
}
var posteriorFindTolerance = 0.00001;
function withinTolerance(v, w) {
return Math.abs(v - w) < posteriorFindTolerance;
}
function bayesFind(variable, posteriorValue) {
// Use binary search.
var min = 0;
var max = 1;
for (;;) {
var middle = (max + min) / 2;
if (withinTolerance(middle, 0)) {
return 0;
}
if (withinTolerance(middle, 1)) {
return 1;
}
var v = bayesAt(variable, middle);
if (withinTolerance(v, posteriorValue)) {
return middle;
}
if (v < posteriorValue) {
min = middle;
} else {
max = middle;
}
}
}
function between(x, lower, upper) {
return lower <= x && x <= upper;
}
function square(x) {
return x * x;
}
function clamp(value) {
return Math.max(0, Math.min(value, 1));
}
Object.defineProperty(model, "fScore", {
get: function() {
var sum = sensitivity + specificity;
return sum === 0 ? 0 : 2 * sensitivity * specificity / sum;
}
});
Object.defineProperty(model, "posterior", {
get: function() {
return bayes(prior, sensitivity, specificity);
},
set: function(value) {
// Use hill climbing.
var maxIterations = 100000;
var samples = 1000;
var wiggliness = 100;
var posteriorWeight = 1000000;
var basePrior = prior;
var baseSensitivity = sensitivity;
var baseSpecificity = specificity;
function objective(prior, sensitivity, specificity) {
var posterior = bayes(prior, sensitivity, specificity);
return square(posterior - value) * posteriorWeight +
square(prior - basePrior) +
square(sensitivity - baseSensitivity) +
square(specificity - baseSpecificity);
}
function wiggle(v) {
return clamp(v + wiggliness * posteriorFindTolerance *
(Math.random() - 0.5));
}
for (var i = 0; i < maxIterations; ++i) {
if (withinTolerance(posterior, value)) {
break;
}
var bestPrior = prior;
var bestSensitivity = sensitivity;
var bestSpecificity = specificity;
var bestValue = objective(prior, sensitivity, specificity);
for (var j = 0; j < samples; ++j) {
// Wiggle independent variables.
var p = wiggle(prior);
var sn = wiggle(sensitivity);
var sp = wiggle(specificity);
// Choose the better.
var newValue = objective(p, sn, sp);
if (newValue < bestValue) {
bestValue = newValue;
bestPrior = p;
bestSensitivity = sn;
bestSpecificity = sp;
}
}
prior = bestPrior;
sensitivity = bestSensitivity;
specificity = bestSpecificity;
}
}
});
// extraPush helps posterior round toward amount.
function nudge(variable, amount, extraPush) {
if (extraPush === undefined) {
extraPush = 0;
}
var value = model[variable];
var clamped = Math.floor(value / amount) * amount;
model[variable] = clamp(clamped + amount + extraPush);
}
/*** Perspective ***/
var selectedVariable = "prior"; // one of the independent variables.
var selectedPosterior = false; // a separate selection.
var controlState = "inert"; // one of "inert", "focused", "active".
var explicitFocusValue = null;
var selectedGuideText = false;
var dynamicSelections = [];
function dynamic(selection, redrawFn) {
if (typeof redrawFn == "undefined") {
redrawFn = selection;
selection = null;
}
dynamicSelections.push({
selection: selection,
redraw: redrawFn
});
redrawFn(selection);
return selection;
}
var redrawRequested = null;
function redraw() {
if (redrawRequested === null) {
redrawRequested = requestAnimationFrame(redrawNow);
}
}
function redrawNow() {
for (var i = 0; i < dynamicSelections.length; ++i) {
dynamicSelections[i].redraw(dynamicSelections[i].selection);
}
redrawRequested = null;
}
function transition(states, d, i) {
var transitionFn = states[controlState];
if (typeof transitionFn !== "function") {
return;
}
return transitionFn(d, i);
}
function on(selection, type, listener) {
if (typeof listener !== "function") {
var states = listener;
listener = function(d, i) {
return transition(states, d, i);
}
}
return selection.on(type, function(d, i) {
var newState = listener(d, i);
if (typeof newState == "string") {
controlState = newState;
}
redraw();
});
}
/*** View ***/
var width = 380;
var height = 380;
var margin = {
top: 10, // just for spacing.
right: 40, // for overflow of xGuideText.
bottom: 100, // for prior, sensitivity, and specificity sliders.
left: 100, // for the posterior slider.
};
var x = d3.scale.linear().range([0, width]);
var y = d3.scale.linear().range([height, 0]);
function xAxis() {
return d3.svg.axis()
.scale(x)
.orient("bottom")
.ticks(10);
}
function yAxis() {
return d3.svg.axis()
.scale(y)
.orient("left")
.ticks(10);
}
var svg = d3.select("main").append("svg")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom)
var svgg = svg.append("g")
.attr("transform", "translate("+margin.left+","+margin.top+")");
/** Grid **/
// Draw the x grid lines.
svgg.append("g")
.classed("grid", true)
.attr("transform", "translate(0,"+height+")")
.call(xAxis()
.tickSize(-height, 0, 0)
.tickFormat(""))
// Draw the y grid lines.
svgg.append("g")
.classed("grid", true)
.call(yAxis()
.tickSize(-width, 0, 0)
.tickFormat(""));
svgg.append("g")
.classed("axis", true)
.attr("transform", "translate(0," + height + ")")
.call(xAxis());
svgg.append("g")
.classed("axis", true)
.call(yAxis());
/** Guide **/
var xLabelOffsetBelowXAxis = 36;
var yLabelOffsetLeftOfYAxis = 60;
var labelOffsetFromLine = 3;
var guideVariable;
var guideValue;
var guideRangeValue;
var guideLineStarts = {};
dynamic(function() {
guideVariable = controlState === "inert" ?
null : selectedVariable;
guideValue = controlState === "active" ||
explicitFocusValue === null ?
model[selectedVariable] : explicitFocusValue;
guideRangeValue = bayesAt(selectedVariable, guideValue);
});
var guide = svgg.append("g")
.classed("guide", true);
function ifGuideShouldAppear(selection, updates) {
if (guideVariable !== null || selectedGuideText) {
selection.attr("display", null)
.call(updates);
} else {
selection.attr("display", "none");
}
}
var verticalGuideLine = guide.append("line")
.call(dynamic, function(s) {s
.call(ifGuideShouldAppear, function(s) {s
.attr("x1", x(guideValue))
.attr("x2", x(guideValue))
.attr("y1", guideLineStarts[selectedVariable])
.attr("y2", y(guideRangeValue));
});
});
var horizontalGuideLine = guide.append("line")
.call(dynamic, function(s) {s
.call(ifGuideShouldAppear, function(s) {s
.attr("x1", guideLineStarts["posterior"])
.attr("x2", x(guideValue))
.attr("y1", y(guideRangeValue))
.attr("y2", y(guideRangeValue));
});
});
var guideFormat = d3.format(".3f");
var xGuideText = guide.append("text")
.attr("y", y(0) + xLabelOffsetBelowXAxis)
.attr("dx", labelOffsetFromLine)
.call(dynamic, function(s) {s
.call(ifGuideShouldAppear, function(s) {s
.attr("x", x(guideValue))
.text(guideFormat(guideValue));
});
});
var yGuideText = guide.append("text")
.attr("x", x(0) - yLabelOffsetLeftOfYAxis)
.attr("dy", -labelOffsetFromLine)
.call(dynamic, function(s) {s
.call(ifGuideShouldAppear, function(s) {s
.attr("y", y(guideRangeValue))
.text(guideFormat(guideRangeValue));
});
});
dynamic(function() {
if (!selectedGuideText) {
return;
}
d3.select("body").classed("disable-user-select", false);
var node = (selectedPosterior ? yGuideText : xGuideText).node();
range = document.createRange();
// range.selectNode often selects too much.
range.setStart(node, 0);
range.setEnd(node, 1);
selection = window.getSelection();
selection.removeAllRanges();
selection.addRange(range);
});
/** Series **/
var seriesResolution = 200;
var seriesPoints = [];
for (var i = 0; i <= seriesResolution; ++i) {
seriesPoints.push(i / seriesResolution);
}
// Separate groups to ensure that circles are on top of all paths.
var seriesPaths = svgg.append("g")
.classed("series", true);
var seriesCircles = svgg.append("g")
.classed("series", true);
function series(variable) {
var line = d3.svg.line()
.x(x)
.y(function(d) { return y(bayesAt(variable, d)); });
seriesPaths.append("path")
.classed(variable, true)
.datum(variable)
.call(dynamic, function(s) {s
.attr("d", line(seriesPoints));
});
seriesCircles.append("circle")
.classed(variable, true)
.datum(variable)
.attr("r", 5)
.call(dynamic, function(s) {s
.attr("cx", x(model[variable]))
.attr("cy", y(posterior));
});
}
series("specificity");
series("sensitivity");
series("prior");
/** Slider Thumb Shadow **/
var sliderThumbShadow = svgg.append("filter")
.attr("id", "slider-thumb-shadow")
// Tell SVG how big the filter might be.
.attr("x", "-50%")
.attr("y", "-50%")
.attr("width", "200%")
.attr("height", "200%");
sliderThumbShadow.append("feGaussianBlur")
.attr("in", "SourceAlpha")
.attr("stdDeviation", 1.1)
.attr("result", "blur");
sliderThumbShadow.append("feOffset")
.attr("in", "blur")
.attr("dy", "1")
.attr("result", "shadow");
var shadowMerge = sliderThumbShadow.append("feMerge");
shadowMerge.append("feMergeNode").attr("in", "shadow");
shadowMerge.append("feMergeNode").attr("in", "SourceGraphic");
/** Slider **/
var thumbMajor = 10;
var thumbMinor = 16;
var hitZoneExtension = 20;
var sliderXProperties = {
v: "x", v1: "x1", v2: "x2",
scale: x, dimension: "width",
r: "rx", c: "cx"
};
var sliderYProperties = {
v: "y", v1: "y1", v2: "y2",
scale: y, dimension: "height",
r: "ry", c: "cy"
};
function slider(variable, offset, orientation) {
if (orientation === undefined) {
orientation = "horizontal";
}
switch (orientation) {
case "horizontal":
var major = sliderXProperties;
var minor = sliderYProperties;
break;
case "vertical":
var major = sliderYProperties;
var minor = sliderXProperties;
break;
default:
throw new TypeError("invalid orientation "+orientation);
}
var slider = svgg.append("g")
.classed("slider", true)
.classed(variable, true)
.datum(variable);
slider.append("line")
.classed("track", true)
.classed("maximum", true)
.attr(major.v1, major.scale(0))
.attr(major.v2, major.scale(1))
.attr(minor.v1, offset)
.attr(minor.v2, offset);
slider.append("line")
.classed("track", true)
.classed("minimum", true)
.attr(major.v1, major.scale(0))
.attr(minor.v1, offset)
.attr(minor.v2, offset)
.call(dynamic, function(s) {s
.attr(major.v2, major.scale(model[variable]));
});
slider.append("rect")
.classed("hit-zone", true)
.attr(major.v,
Math.min(major.scale(0), major.scale(1)) - hitZoneExtension)
.attr(major.dimension,
Math.abs(major.scale(1) - major.scale(0)) + 2 * hitZoneExtension)
.attr(minor.v, offset - thumbMinor / 2)
.attr(minor.dimension, thumbMinor);
slider.append("ellipse")
.classed("thumb", true)
.attr(major.r, thumbMajor / 2)
.attr(minor.r, thumbMinor / 2)
.attr("filter", "url(#slider-thumb-shadow)")
.attr(minor.c, offset)
.call(dynamic, function(s) {s
.attr(major.c, major.scale(model[variable]))
.style("stroke", function(d) {
return selectedVariable === d ? "black" : null;
});
});
guideLineStarts[variable] = offset;
}
var sliderSpacing = 20;
var firstSliderY = 430;
slider("prior", firstSliderY);
slider("sensitivity", firstSliderY + sliderSpacing);
slider("specificity", firstSliderY + sliderSpacing * 2);
slider("posterior", -70, "vertical")
/** Cursor **/
d3.selectAll("main svg, body")
.call(dynamic, function(s) {s
.style("cursor", controlState === "inert" ? null :
selectedPosterior ? "row-resize" : "col-resize");
});
// Don't want to select axis labels while dragging.
d3.select("body")
.call(dynamic, function(s) {s
.classed("disable-user-select",
controlState !== "inert" && !selectedGuideText);
});
/*** Controller ***/
function mousePositionValue(scale) {
if (scale === undefined) {
scale = x;
}
var m = d3.mouse(svgg.node());
return clamp(scale.invert(m[scale === x ? 0 : 1]));
}
// To prevent moveover and mousemove from interfering with each other.
var ignoreNextMouseMove = false;
function focusOn(variable) {
if (variable === "posterior") {
selectedPosterior = true;
} else {
selectedPosterior = false;
selectedVariable = variable;
}
ignoreNextMouseMove = true;
explicitFocusValue = null;
return "focused";
}
function focusOnWithValueFromMousePosition(variable) {
focusOn(variable);
explicitFocusValue = selectedPosterior ?
bayesFind(selectedVariable, mousePositionValue(y)) :
mousePositionValue();
return "focused";
}
if (svg.node().checkIntersection === undefined) {
function overlaps(t1, t2, x1, x2) {
return t1 < x1 ? t2 >= x1 : t1 <= x2;
}
// For Firefox, hack an adequate alternative.
svg.node().checkIntersection = function(element, rect) {
var left = rect.x - margin.left;
var right = left + rect.width;
var top = rect.y - margin.top;
var bottom = top + rect.height;
if (element === horizontalGuideLine.node()) {
var y = +horizontalGuideLine.attr("y1");
var x1 = +horizontalGuideLine.attr("x1");
var x2 = +horizontalGuideLine.attr("x2");
return between(y, top, bottom) &&
overlaps(left, right, x1, x2);
} else {
var x = +verticalGuideLine.attr("x1");
// Index reversed since we draw stroke upwards.
var y1 = +verticalGuideLine.attr("y2");
var y2 = +verticalGuideLine.attr("y1");
return between(x, left, right) &&
overlaps(top, bottom, y1, y2);
}
}
}
var isNearThreshhold = 20;
var isNearRect = svg.node().createSVGRect();
isNearRect.width = isNearRect.height = 2 * isNearThreshhold;
function isNear(selection) {
var m = d3.mouse(svg.node());
isNearRect.x = m[0] - isNearThreshhold;
isNearRect.y = m[1] - isNearThreshhold;
return svg.node().checkIntersection(selection.node(), isNearRect);
}
function movedAway(selection) {
return !isNear(selection);
}
function becomeFocusedIfIsNearGuideLines() {
return (isNear(verticalGuideLine) ||
isNear(horizontalGuideLine)) && "focused";
}
function becomeInertIfMovedAwayFromGuideLines() {
if (isNear(verticalGuideLine)) {
selectedPosterior = false;
} else if (isNear(horizontalGuideLine)) {
selectedPosterior = true;
}
return movedAway(verticalGuideLine) &&
movedAway(horizontalGuideLine) && "inert";
}
function updateSelectedVariableFromMousePosition() {
if (selectedPosterior) {
posterior = mousePositionValue(y);
} else {
model[selectedVariable] = mousePositionValue();
}
}
function activate(variable) {
if (variable === "posterior") {
selectedPosterior = true;
} else if (controlState !== "focused" && variable !== undefined) {
selectedVariable = variable;
}
explicitFocusValue = null;
selectedGuideText = false;
updateSelectedVariableFromMousePosition();
return "active";
}
svgg.selectAll(".series circle, .slider .thumb")
.call(on, "mouseover", {
inert: focusOn,
focused: focusOn
})
.call(on, "dblclick", function(variable) {
selectedGuideText = true;
});
svgg.selectAll(".series path, .slider .hit-zone")
.call(on, "mousemove", {
inert: focusOnWithValueFromMousePosition,
focused: focusOnWithValueFromMousePosition
})
.call(on, "mousedown", activate);
d3.select("main")
.call(on, "mousemove", function() {
if (ignoreNextMouseMove) {
ignoreNextMouseMove = false;
return;
}
return transition({
inert: becomeFocusedIfIsNearGuideLines,
focused: becomeInertIfMovedAwayFromGuideLines,
active: updateSelectedVariableFromMousePosition
});
})
.call(on, "mousedown", {
inert: function() {
selectedGuideText = false;
},
focused: activate
})
.call(on, "mouseup", {
active: d3.functor("focused")
});
d3.select("body")
.call(on, "keydown", function() {
switch (d3.event.keyCode) {
case 37: // left
nudge(selectedVariable, -0.001);
break;
case 39: // right
nudge(selectedVariable, 0.001);
break;
case 38: // up
nudge("posterior", 0.001, posteriorFindTolerance);
break;
case 40: // down
nudge("posterior", -0.001, -posteriorFindTolerance);
break;
default:
return;
}
controlState = "focused";
explicitFocusValue = null;
d3.event.preventDefault(); // to avoid scrolling.
});
body {
margin: 0;
font: 13px "Helvetica Neue", Helvetica, sans-serif;
text-align: center;
}
main {
display: flex;
justify-content: center;
min-height: 500px;
min-width: 800px;
}
#details {
width: 250px;
height: 480px;
text-align: left;
display: flex;
flex-direction: column;
}
a:not(:hover) {
text-decoration: none;
}
a {
color: steelblue;
}
#example {
flex: 1;
}
#example li {
margin-left: -2em;
}
#example ul div {
margin-left: 0.5em;
}
address {
font-style: normal;
}
/** Colors **/
.posterior {
color: hsl(120, 95%, 38%);
fill: hsl(120, 95%, 38%);
stroke: hsl(120, 95%, 38%);
}
.prior {
color: hsl(0, 95%, 38%);
fill: hsl(0, 95%, 38%);
stroke: hsl(0, 95%, 38%);
}
.sensitivity {
color: hsl(200, 95%, 38%);
fill: hsl(200, 95%, 38%);
stroke: hsl(200, 95%, 38%);
}
.specificity {
color: hsl(280, 95%, 38%);
fill: hsl(280, 95%, 38%);
stroke: hsl(280, 95%, 38%);
}
/** Grid **/
.grid .tick {
stroke: lightgrey;
}
.axis path,
.axis line {
fill: none;
stroke: grey;
}
/** Backing **/
.hit-zone {
stroke: none;
fill: none;
pointer-events: visible;
}
/** Guide **/
.guide {
stroke: grey;
stroke-width: 1;
stroke-dasharray: 6, 4;
}
.guide text {
fill: grey;
stroke: none;
}
/** Series **/
.series path {
fill: none;
stroke-width: 2;
stroke-linecap: round;
}
.series circle {
stroke: none;
}
/** Slider **/
.slider .track {
fill: none;
stroke-width: 4;
stroke-linecap: round;
}
.slider .track.maximum {
stroke: lightgrey;
}
.slider .thumb {
stroke-width: 0.5;
stroke: none;
}
/** Cursor **/
main svg {
cursor: default;
}
.disable-user-select {
-webkit-touch-callout: none;
-webkit-user-select: none;
-khtml-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment