Published
Edited
May 4, 2020
3 stars
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
function main(){
/* VARIABLE DEFINITIONS */

var IS_MOUSE_DOWN = false
var PREVIOUS_XCLICK, PREVIOUS_YCLICK
var UPDATING_PLOT
var CANVASDIST
var DISTRO_EV, DISTRO_SD
var CANV_COMMANDS_ENABLED

var canvas = document.getElementById('canvasID')
var CTX = canvas.getContext('2d')
var RECT = canvas.getBoundingClientRect()


var X_LIM = canvas.width
var X0_SPACE_FACTOR = 15
var X0 = Math.round(X_LIM / X0_SPACE_FACTOR)
var X0B = X0 - 1
var X1 = X_LIM - X0
var X1B = X1 - 1

var Y_LIM = canvas.height
var Y0_RATIO = 0.9
var Y0_LINE = Math.round(Y_LIM * Y0_RATIO)
var MAX_PROB = Math.round(Y_LIM * (1 - Y0_RATIO))

var PADDING10 = 10
var PADDING4 = 4

var PRECISION = X_LIM - 2 * X0
var PRECISIONB = PRECISION - 1

let leftConfidenceInterval = 0
let rightConfidenceInterval = 1000

/* PLOTTING FUNCTIONS */

function percentiles(i, j) {
const interval = CANVASDIST.reduce((total, x) => { return (total + (Y0_LINE - x[1])) }, 0) / j
const negInterval = interval * -1 + 20
let startIndex = 0
let firstVisit = true
let firstVisit2 = true
let probRemaining = (i - 1) * interval
let out
CANVASDIST.forEach((x, i) => {
probRemaining -= Y0_LINE - x[1] // probability mass in column
if (probRemaining <= 0) {
if (firstVisit) {
startIndex = i
firstVisit = false
}
if (probRemaining <= negInterval) {
if (firstVisit2) {
out = [startIndex, i]
firstVisit2 = false
}
}
}
})
return out
}
function plotCanvasDist(CANVASDIST, CTX) {
console.log(CANVASDIST)
// clears pixels w/ new white sheet.
CTX.fillStyle = '#FFF'
CTX.fillRect(0, 0, X_LIM, Y_LIM)
CTX.fillStyle = '#000'
CTX.stroke()
// number of equaprobability shaded percentiles
const j = 5
for (let i = 1; i < j + 1; i++) {
CTX.beginPath()
const temp = percentiles(i, j)
const startIndex = temp[0]
const endIndex = temp[1]
CTX.strokeStyle = i % 2 === 1 ? '#DDD' : '#EEE'
for (let i = startIndex; i < endIndex; i++) {
CTX.moveTo(X0 + CANVASDIST[i][0], Y0_LINE)
CTX.lineTo(X0 + CANVASDIST[i][0], CANVASDIST[i][1])
}
CTX.moveTo(X0 + CANVASDIST[endIndex][0], Y0_LINE)
CTX.lineTo(X0 + CANVASDIST[endIndex][0], CANVASDIST[endIndex][1])
CTX.stroke()
}

// draws lines parallel to x axis + factors to help w/ precise drawing.
CTX.beginPath()
CTX.strokeStyle = '#CCC'
CTX.lineWidth = 2.0
CTX.moveTo(X0, Y0_LINE)
CTX.lineTo(X1, Y0_LINE)
CTX.font = '18px Roboto'
CTX.textAlign = 'left'
CTX.fillText('0', X0B + PADDING4, Y_LIM * (Y0_RATIO - 0.01))
CTX.moveTo(X0B, Y_LIM * (Y0_RATIO - 0.1))
CTX.lineTo(X1, Y_LIM * (Y0_RATIO - 0.1))
CTX.fillText('1x', X0B + PADDING4, Y_LIM * (Y0_RATIO - 0.11))
CTX.moveTo(X0B, Y_LIM * (Y0_RATIO - 0.2))
CTX.lineTo(X1, Y_LIM * (Y0_RATIO - 0.2))
CTX.fillText('2x', X0B + PADDING4, Y_LIM * (Y0_RATIO - 0.21))
CTX.moveTo(X0B, Y_LIM * (Y0_RATIO - 0.3))
CTX.lineTo(X1, Y_LIM * (Y0_RATIO - 0.3))
CTX.fillText('3x', X0B + PADDING4, Y_LIM * (Y0_RATIO - 0.31))
CTX.moveTo(X0B, Y_LIM * (Y0_RATIO - 0.4))
CTX.lineTo(X1, Y_LIM * (Y0_RATIO - 0.4))
CTX.fillText('4x', X0B + PADDING4, Y_LIM * (Y0_RATIO - 0.41))
CTX.moveTo(X0B, Y_LIM * (Y0_RATIO - 0.5))
CTX.lineTo(X1, Y_LIM * (Y0_RATIO - 0.5))
CTX.fillText('5x', X0B + PADDING4, Y_LIM * (Y0_RATIO - 0.51))
CTX.moveTo(X0B, Y_LIM * (Y0_RATIO - 0.6))
CTX.lineTo(X1, Y_LIM * (Y0_RATIO - 0.6))
CTX.fillText('6x', X0B + PADDING4, Y_LIM * (Y0_RATIO - 0.61))
CTX.moveTo(X0B, Y_LIM * (Y0_RATIO - 0.7))
CTX.lineTo(X1, Y_LIM * (Y0_RATIO - 0.7))
CTX.fillText('7x', X0B + PADDING4, Y_LIM * (Y0_RATIO - 0.71))
CTX.stroke()
// dashed mu line
const xEV = Math.floor(CANVASDIST.reduce((total, x) => { return (total + x[0] * (Y0_LINE - x[1])) }, 0) / CANVASDIST.reduce((total, x) => { return (total + (Y0_LINE - x[1])) }, 0))
CTX.beginPath()
CTX.strokeStyle = '#5680cc'
CTX.setLineDash([5, 10])
CTX.moveTo(X0 + xEV, CANVASDIST[xEV][1])
CTX.lineTo(X0 + xEV, Y0_LINE)
CTX.stroke()
CTX.setLineDash([])
// draws blue distribution
CTX.beginPath()
CTX.strokeStyle = '#5680cc'
CTX.lineWidth = 3
CTX.moveTo(X0 + CANVASDIST[0][0], CANVASDIST[0][1])
for (let i = 0; i < PRECISION; i++) {
CTX.lineTo(X0 + CANVASDIST[i][0], CANVASDIST[i][1])
}
CTX.stroke()

// draws XUNITS, y units (Pr()), xVals along x-axis
CTX.beginPath()
CTX.strokeStyle = '#000'
CTX.font = '30px Roboto'
CTX.textAlign = 'right'
CTX.strokeText('Pr', X0 - 1.5 * PADDING10, Y_LIM * 0.46)
CTX.textAlign = 'center'
CTX.strokeText('X', X_LIM * 0.5, Y_LIM * 0.985)
CTX.stroke()

// draws units along the x axis
CTX.font = '16px Roboto'
CTX.lineWidth = 2.0
for (let i = 0; i < 13; i++) {
let x = Math.round(PRECISION * i / (X0_SPACE_FACTOR - 3))
if (x > PRECISIONB) { x = PRECISIONB }
CTX.fillText(format(CANVASDIST[x][2]), X0 + x, Y_LIM * (Y0_RATIO + 0.036))
if (x === 0) { x = -2 }
if (x === PRECISIONB) { x += 2 }
CTX.moveTo(X0 + x, Y_LIM * (Y0_RATIO + 0.01))
CTX.lineTo(X0 + x, Y_LIM * (Y0_RATIO - 0.01))
}
CTX.stroke()

// draws rectangle around the plot
CTX.lineWidth = 2.0
CTX.strokeStyle = '#000'
CTX.moveTo(X1, Y0_LINE)
CTX.lineTo(X0 - 2, Y0_LINE)
CTX.lineTo(X0 - 2, Y_LIM * 0.1 - 1)
CTX.lineTo(X1 + 1, Y_LIM * 0.1 - 1)
CTX.lineTo(X1 + 1, Y0_LINE)
CTX.stroke()
}


/* DEFINE THE INITIAL DISTRIBUTION*/

function initializeDistribution(leftConfidenceInterval, rightConfidenceInterval, confidence) {
function stanNormCDF(length, zScore) {
const probabilities = []
const e = Math.E
const aConst = zScore * 2 / length
let x, prob
let temp = 0
for (let i = 0; i < length; i++) {
x = i * aConst - zScore
prob = 1 / (1 + e ** (0.0054 - 1.6101 * x - 0.0674 * x ** 3)) // approximation from http://web2.uwindsor.ca/math/hlynka/zogheibhlynka.pdf
probabilities.push(prob - temp)
temp = prob
}
probabilities[0] = 0
return probabilities
}
function genNormDistroFromCI(leftVal, rightVal, confidenceLevel, PRECISION) {
function confidenceToZscore(c = 0.95) {
switch (c) {
case 0.8: return 0.845
case 0.9: return 1.645
case 0.95: return 1.96
case 0.98: return 2.33
case 0.99: return 2.575
case 1.0: return 3.0
default: return 1.96
}
}
const zScore = 3.0
const standardDeviation = (rightVal - leftVal) * 0.5 / confidenceToZscore(confidenceLevel)
const leftMostX = (leftVal + rightVal) * 0.5 - zScore * standardDeviation
const xConversionFactor = 2 * zScore * standardDeviation / PRECISION
const probs = stanNormCDF(PRECISION, zScore)
return probs.map((p, i) => { return [leftMostX + i * xConversionFactor, p] })
}

function distroToCANVASDIST(distro) {
const maxProb = distro.reduce((highest, x) => { return highest > x[1] ? highest : x[1] }, 0)
const leftMostX = distro[0][0]
const xConversionFactor = PRECISION / (distro[PRECISION - 1][0] - leftMostX)
const yConversionFactor = Y_LIM / (2 * maxProb)
return distro.map((x, i) => {
return [Math.round(xConversionFactor * (x[0] - leftMostX) - 1), Math.round(Y0_LINE - yConversionFactor * x[1]), x[0]]
})
}

let distro = genNormDistroFromCI(leftConfidenceInterval, rightConfidenceInterval, confidence, PRECISION)
CANVASDIST = distroToCANVASDIST(distro)
plotCanvasDist(CANVASDIST, CTX)

}

initializeDistribution(leftConfidenceInterval, rightConfidenceInterval, 0.95)


/* ADD EVENT LISTENERS*/
function updateDistWithMouseMovement(xClick, yClick, CANVASDIST, PREVIOUS_XCLICK, PREVIOUS_YCLICK) {
/*
The mouse moves across the screen, and we get a series of (x,y) positions.
We know where the mouse last was
we update everything between the last (x,y) position and the new (x,y), using linear interpolation
*/
xClick -= X0
PREVIOUS_XCLICK -= X0
const avgYShift = (yClick - PREVIOUS_YCLICK) / (xClick - PREVIOUS_XCLICK)
let j = 1
if (xClick > PREVIOUS_XCLICK) {
for (let i = PREVIOUS_XCLICK; i < xClick; i++) {
CANVASDIST[i][1] = PREVIOUS_YCLICK + j * avgYShift
j++
}
} else {
for (let i = PREVIOUS_XCLICK; i > xClick; i--) {
CANVASDIST[i][1] = PREVIOUS_YCLICK - j * avgYShift
j++
}
}
}

canvas.addEventListener('mousedown', e => { // to add touch support for touch screens // basically copy, but with ~'touchdown'
//displayHoverVal(e)
RECT = canvas.getBoundingClientRect() // gets new coords of canvas considering user could scroll page
if (e.clientX - RECT.left > X0 && e.clientX - RECT.left < X0 + PRECISION) {
PREVIOUS_XCLICK = Math.round(e.clientX - RECT.left)
CANV_COMMANDS_ENABLED = true
} else {
PREVIOUS_XCLICK = null
CANV_COMMANDS_ENABLED = false
}
if (e.clientY - RECT.top < Y0_LINE && e.clientY - RECT.top > MAX_PROB) {
PREVIOUS_YCLICK = e.clientY - RECT.top
} else {
PREVIOUS_YCLICK = null
CANV_COMMANDS_ENABLED = false
}
IS_MOUSE_DOWN = true

UPDATING_PLOT = window.setInterval(() => {
plotCanvasDist(CANVASDIST, CTX) // updates canvas every 40ms
}, 40)
})

canvas.addEventListener('mousemove', e => {
//displayHoverVal(e)
if (IS_MOUSE_DOWN === true) {
let xClick = Math.round(e.clientX - RECT.left)
let yClick = e.clientY - RECT.top
if (PREVIOUS_XCLICK === null) {
PREVIOUS_XCLICK = xClick
}
if (PREVIOUS_YCLICK === null) {
PREVIOUS_YCLICK = yClick
}
if (xClick < X0) { xClick = X0 }
if (xClick > X1B) { xClick = X1B }
if (yClick > Y0_LINE) { yClick = Y0_LINE }
if (yClick < MAX_PROB) { yClick = MAX_PROB }
updateDistWithMouseMovement(xClick, yClick, CANVASDIST, PREVIOUS_XCLICK, PREVIOUS_YCLICK)
PREVIOUS_XCLICK = xClick
PREVIOUS_YCLICK = yClick
}
})

window.addEventListener('mouseup', e => {
//displayHoverVal(e)
if (IS_MOUSE_DOWN === true) {
IS_MOUSE_DOWN = false
window.clearInterval(UPDATING_PLOT)
plotCanvasDist(CANVASDIST, CTX)

RECT = canvas.getBoundingClientRect()
}
})

/* Add event listeners for touchscreens*/

canvas.addEventListener('touchstart', e => { e.preventDefault() })
canvas.addEventListener('touchmove', e => { e.preventDefault() })
canvas.addEventListener('touchend', e => { e.preventDefault() })
canvas.addEventListener('touchcancel', e => { e.preventDefault() })

canvas.addEventListener('touchstart', e => {
CANV_COMMANDS_ENABLED = true
const touch = e.touches[0]
const mouseEvent = new MouseEvent('mousedown', {
clientX: touch.clientX,
clientY: touch.clientY
})
canvas.dispatchEvent(mouseEvent)
}, false)

canvas.addEventListener('touchend', e => {
const mouseEvent = new MouseEvent('mouseup', {})
canvas.dispatchEvent(mouseEvent)
}, false)

canvas.addEventListener('touchmove', e => {
const touch = e.touches[0]
const mouseEvent = new MouseEvent('mousemove', {
clientX: touch.clientX,
clientY: touch.clientY
})
canvas.dispatchEvent(mouseEvent)
}, false)


/* Event listeners for buttons */

document.getElementById('genVarBtn').addEventListener('click', function () {
const left = parseFloat(document.getElementById('leftVal').value)
const right = parseFloat(document.getElementById('rightVal').value)
const confidence = parseFloat(document.querySelector('input[name="confidenceRadio"]:checked').value)
console.log({ left, right, confidence })
initializeDistribution(left, right, confidence)
plotCanvasDist(CANVASDIST, CTX)
})
document.getElementById('createFromGuesstimateString').addEventListener('click', function () {
const guesstimateString = document.getElementById('guesstimateString').value
console.log(guesstimateString)
let cdf = guesstimatorInputToCdf({
guesstimatorInput: guesstimateString,
simulationSampleCount: 10000,
outputSampleCount: 1000,
smoothingWidth: 80
})
let pdf = cdf.toPdf()
CANVASDIST = XYDistToCanvasDist(pdf)
console.log("Create from Guesstimate")
console.log(cdf)
console.log(pdf)
console.log(CANVASDIST)
plotCanvasDist(CANVASDIST, CTX)
})
document.getElementById('sendToForetoldButton').addEventListener('click', function () {
const questionID = document.getElementById('measurableId').value
const token = document.getElementById('foretoldToken').value
const comment = document.getElementById('comment').value

let XYDIST = canvasToXYDist(CANVASDIST);
let cdf = XYDistToCdf(XYDIST)
predict({ questionID, token, cdf, comment})
})

/* NUMBER FORMATTING */
function numberWithCommas(x) {
return x.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',')
}
function atLeast3SigFigs(number) {
var formattedNumber;
const nstring = number.toString()
for (let i = 0; i < nstring.length; i++) {
if (nstring[i] === '.') {
formattedNumber = i > 3 ? Math.round(number) : Math.round((number + Number.EPSILON) * 10 ^ (3 - i)) / 10 ^ (3 - i)
return formattedNumber
}
}
return Math.round(number)
}
function format(number) {
return numberWithCommas(atLeast3SigFigs(number))
}

/*CONVERT DISTRIBUTIONS TO AND FROM FORETOLD FORMAT*/

/*
The fundamental unit for the canvas is the distance vector from the (0,0) point at the upper leftmost corner of the screen
The fundamental unit for a probability distribution is an x coordinate and its corresponding y probability density
*/

function canvasToXYDist(CANVASDIST) {
let xs = []
let ys = []
for (let point of CANVASDIST) {
console.log(point)
xs.push(point[2])
ys.push(Y0_LINE - point[1])

}
/* Note that normalization here is tricky.
The code that follows doesn't quite work;
we'd need to choose either StepWise or Linear interpolation,
and integrate according to that interpolation.*/
let sumYs = ys.reduce((x, y) => x + y, 0);
ys = ys.map(y => y / sumYs);

return ({ xs, ys })
}

function XYDistToCdf(XYDIST) {
const cumulativeSum = (sum => value => sum += value)(0);
const cdfYs = (XYDIST.ys).map(cumulativeSum);
return ({ xs: XYDIST.xs, ys: cdfYs })
}
function XYDistToCanvasDist(pdf){
let canvas = []
let minY = 0
let maxY = pdf.ys.reduce((a,b) => a>b?a:b, 0)
let ysScaled = pdf.ys.map(x=> (x/maxY)*Y0_LINE*0.85);
let ysInversed = ysScaled.map(x => Y0_LINE-x);

for(let i=0; i<pdf.xs.length-1; i++){
let point = []
point.push(i)
point.push(ysInversed[i])
point.push(pdf.xs[i])
canvas.push(point)
}
return canvas

}

// conclude the main function
return "Increasing the warp factor"
}
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell

Purpose-built for displays of data

Observable is your go-to platform for exploring data and creating expressive data visualizations. Use reactive JavaScript notebooks for prototyping and a collaborative canvas for visual data exploration and dashboard creation.
Learn more