Public
Edited
May 18, 2024
2 forks
4 stars
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
function buildPlot(data) {
///////////////////// SETUP /////////////////////
// Create internal data array (scoped within this function)
let idCounter = 0; // To give each data point a unique id
const accesX = ([x]) => x; // accessor function for x-coordinate
const accesY = ([, y]) => y; // accessor function for y-coordinate
const accesType = ([, , type]) => type; // accessor function for type
// There is probably a more elegant way to set up data
data = [{xCoord: null, yCoord: null, type: null, id: null}];
console.log("init");
data = data.map((d) => ({
xCoord: d.xCoord,
yCoord: d.yCoord,
type: d.type,
id: idCounter++
}));
data.pop(); // make the object empty
// Store copy of initial data object
const initData = data.map((d) => ({ ...d }));

///////////////////// DRAW SVG AND AXES /////////////////////
// Draw svg
const svg = d3
.create("svg")
.attr("width", cfg.width)
.attr("height", cfg.height)
.attr("viewBox", [0, 0, cfg.width, cfg.height])
.attr("style", "max-width: 100%; height: auto; height: intrinsic;")
.on("mousemove", mouseOver);
function mouseOver(event) {
let [x, y] = d3.pointer(event);
x = cfg.xScale.invert(x);
y = cfg.yScale.invert(y);

calculateLdaDelta(x, y, data);
d3.select("#xDisplay").text(Math.round(x*100)/100);
d3.select("#yDisplay").text(Math.round(y*100)/100);
}
// Draw Axes
drawAxes(svg)
// Set up variances svg
let varianceRects = svg
.append('g')
.attr('id', 'variancesRectGroup')
.attr('display', displaySettings.value.showParameterCheckbox.indexOf("Variances") !== -1 ? 'block' : 'none' )
.selectAll('rect');

// Set up means svg
let meansSvg = svg
.append("g")
.attr('id', 'meanLineGroup')
.attr('display', displaySettings.value.showParameterCheckbox.indexOf("Means") !== -1 ? 'block' : 'none' );

// Set up legend
const legendCurve = d3.line().curve(d3.curveLinear);
const legend = svg.append("g")
.attr("class", "legend")
.attr("transform", "translate(" + (cfg.width - 120) + "," + 40 + ")");
legend.append("path")
.attr("d", legendCurve([[0,0], [20,0]]))
.attr("stroke", "black")
.attr("stroke-width", 2)
.style("stroke-dasharray", "0, 0")
.attr("fill", "none");
legend.append("text")
.attr("x", 25)
.attr("y", 0)
.text("Estimated boundary")
.style("font-size", "10px");

// second legend
const legend2 = svg.append("g")
.attr("transform", "translate(" + (cfg.width - 120) + "," + (60) + ")");

legend2.append("path")
.attr("d", legendCurve([[0,0], [20,0]]))
.attr("stroke", "black")
.attr("stroke-width", 2)
.style("stroke-dasharray", "3, 3")
.attr("fill", "none");

legend2.append("text")
.attr("x", 25)
.attr("y", 0)
.attr("dy", ".35em")
.text("Bayes boundary")
.style("font-size", "10px");
// Draw space for plot interactions
const plotRect = svg
.append("rect")
.attr("width", cfg.width)
.attr("height", cfg.height)
.attr("opacity", 0)
.on("click", addDatapointByClick);

// Set up circles svg
let circles = svg
.append("g")
.selectAll("circle");
runStartingAnimation(data);
function runStartingAnimation (data) {
// Perform opening animation
const totalTime = 3_000;
const ease = d3.easeLinear;
console.log("run starting animation");
d3.range(0, data0.length).forEach((i) => {;
setTimeout(() => {
addDatapoint(data0[i]['xCoord'], data0[i]['yCoord'], data0[i]['type']);
}, totalTime * ease(i / data0.length));
});

// from here https://dev.to/codebubb/how-to-shuffle-an-array-in-javascript-2ikj
function shuffle(array) {
const newArray = [...array]
const length = newArray.length
for (let start = 0; start < length; start++) {
const randomPosition = Math.floor((newArray.length - start) * Math.random())
const randomItem = newArray.splice(randomPosition, 1)
newArray.push(...randomItem)
}
return newArray
}
}
///////////////////// DRAG /////////////////////
// Init drag object.
const drag = d3
.drag()
.on("drag", dragged)
.on("start", dragstarted)
.on("end", dragended);
// Drag interactions for circles.
function dragstarted(event, d) {
d3.select(this).raise().attr("stroke", "black");
}
function dragged(event, d) {
d3.select(this)
// Update data point, as well as its position on the plot
.attr("cx", () => {
d.xCoord = cfg.xScale.invert(event.x);
return event.x;
})
.attr("cy", () => {
d.yCoord = cfg.yScale.invert(event.y);
return event.y;
});
varianceRects = updateVariances(data, varianceRects);
drawDecisionBoundary(data, svg);
drawQDADecisionBoundary(data, svg);
drawMeans(data, meansSvg);
hideBayesBoundary();
}
function dragended(event, i) {
d3.select(this).attr("stroke", 'null');
}


///////////////////// MAIN FUNCTIONS /////////////////////

// Click interaction for plot.
function addDatapointByClick(event) {
const [xm, ym] = d3.pointer(event);
console.log("add datapoint by click");
let type = -1;
if (displaySettings.value.addDataOptions == "Random") {
type = Math.round(d3.randomUniform()());
} else if (displaySettings.value.addDataOptions == "Red") {
type = 0;
} else if (displaySettings.value.addDataOptions == "Blue") {
type = 1;
}

addDatapoint(cfg.xScale.invert(xm), cfg.yScale.invert(ym), type);

hideBayesBoundary();
}
// Adds a new datapoint and updates the plot
function addDatapoint(xCoord, yCoord, type) {
// Add datapoint
const newValue = { xCoord: xCoord, yCoord: yCoord, type: type, id: idCounter++ };
data = [...data, newValue];
update(data);
}

// Click interaction for circles.
function removeDatapoint(event, dCurr) {
if (event.defaultPrevented) return; // dragged

// Remove data point; faster way to do this?
data = data.filter((d) => d.id !== dCurr.id);

update(data);
hideBayesBoundary();
}
// Resets the plot to the initial data
function reset() {
update(initData, true);
hideBayesBoundary();
}

// Main function that updates the plot based on new data
function update(newData = data) {
// Upate local data object
data = newData.map((d) => ({ ...d }));

varianceRects = updateVariances(data, varianceRects);
circles = updateCircles(data, circles, drag, removeDatapoint);
drawDecisionBoundary(data, svg);
drawQDADecisionBoundary(data, svg);
drawMeans(data, meansSvg);

}

function hideBayesBoundary() {
// searches and hides the Bayes Boundary
// if data are added, dragged or removed
d3.selectAll("*").filter(function() {
return d3.select(this).attr("id") === "ldalineBayes";
}).attr('display', 'none');
}
return Object.assign(svg.node(), {
update,
reset
});
}
Insert cell
function drawAxes(svg) {
// Draw xAxis.
const axisOpacity = 1;

const xGroup = svg
.append("g")
.attr("transform", `translate(0,${cfg.height - cfg.marginBottom})`)
.attr("opacity", axisOpacity)
.call(cfg.xAxis)
.call((g) => g.select(".domain").remove());
// Draw grid lines
xGroup
.selectAll(".tick line")
.clone()
.attr("y2", cfg.marginTop + cfg.marginBottom - cfg.height)
// Draw line at origin
.attr("stroke-opacity", (d) => {
if (d == 0) {
return axisOpacity;
} else {
return cfg.showGrid ? 0.1 : 0;
}
});

// Draw yAxis.
const yGroup = svg
.append("g")
.attr("transform", `translate(${cfg.marginLeft},0)`)
.attr("opacity", axisOpacity)
.call(cfg.yAxis)
.call((g) => g.select(".domain").remove());
yGroup
.selectAll(".tick line")
.clone()
.attr("x2", width - cfg.marginLeft - cfg.marginRight)
// Draw line at origin
.attr("stroke-opacity", (d) => {
if (d == 0) {
return axisOpacity;
} else {
return cfg.showGrid ? 0.1 : 0;
}
});
}
Insert cell
// Helper function to update circles based on new data.
function updateCircles(data, circles, drag, removeDatapoint) {
// circles: existing circles in the svg
// data: data set, which gets compared here with the existing circles
return circles
.data(data, (d) => d.id)
.join( //https://bost.ocks.org/mike/join/
(enter) =>
enter
.append("circle")
.attr("cx", (d) => cfg.xScale(d.xCoord))
.attr("cy", (d) => cfg.yScale(d.yCoord))
.attr("fill", (d) => d.type == 0 ? d3.schemeSet3[3] : d3.schemeSet3[4])
.attr("opacity", cfg.circleOpacity)
// To transition from 0 radius
.attr("r", cfg.r) // no transition
//.attr("r", 0) // transition
// Attach interactions
.call(drag)
.on("click", removeDatapoint)

// Add transition
// #* Somehow doesn't work correctly. For testing this set attr("r", 0) above
// .attr("r", 0) // transition
// .call((enter) =>
// enter
// .transition()
// .duration(300)
// .ease(d3.easeBackOut.overshoot(1.7))
// .attr("r", cfg.r)
// )
,
(update) =>
update
.transition()
.attr("cx", (d) => cfg.xScale(d.xCoord))
.attr("cy", (d) => cfg.yScale(d.yCoord)),
(exit) =>
exit
.transition()
.ease(d3.easeBackIn.overshoot(1.7))
.attr("r", 0)
.remove()
);
}
Insert cell
Insert cell
function calculateLineCoefficients () {

function calculatePriorsPopulation() {
// calculate by looking into data parameters
const DATA_PARAMETERS = getDataParameters();
const type0prior = DATA_PARAMETERS.type0.numPoints/
(DATA_PARAMETERS.type0.numPoints + DATA_PARAMETERS.type1.numPoints);
const type1prior = DATA_PARAMETERS.type1.numPoints/
(DATA_PARAMETERS.type0.numPoints + DATA_PARAMETERS.type1.numPoints);
return {type0prior, type1prior}
}

function calculatePriorsSample(data) {
// calculate priors by counting datapoints
const type0prior = data.filter((d) => d.type === 0).length/data.length;
const type1prior = data.filter((d) => d.type === 1).length/data.length;
return {type0prior, type1prior}
}
function estimatedBoundary(data) {
// calculates the coefficients beta0 and beta1 based on estimated distribution values
const {type0prior, type1prior} = calculatePriorsSample(data);
const [type0Mean, type1Mean, covMatrix] = calculateLdaEstimates(data);
const [v0, v1, z0, z1, logPrior0, logPrior1] = calculateIntermediateValues(
type0Mean, type1Mean, covMatrix, type0prior, type1prior);
const [beta0, beta1] = calculateBeta(v0, v1, z0, z1, logPrior0, logPrior1);


return [beta0, beta1];
}

function bayesBoundary() {
// calculates the coefficients beta0 and beta1 based on actual distribution values
const DATA_PARAMETERS = getDataParameters();
const {type0prior, type1prior} = calculatePriorsPopulation();
const type0Mean = math.matrix([
[DATA_PARAMETERS.type0.x.mean],
[DATA_PARAMETERS.type0.y.mean],
]);
const type1Mean = math.matrix([
[DATA_PARAMETERS.type1.x.mean],
[DATA_PARAMETERS.type1.y.mean],
]);

const XVariance = (
DATA_PARAMETERS.type0.x.variance * type0prior +
DATA_PARAMETERS.type1.x.variance * type1prior);
const YVariance = (
DATA_PARAMETERS.type0.y.variance * type0prior +
DATA_PARAMETERS.type1.y.variance * type1prior);
const OverallXMean = DATA_PARAMETERS.type0.x.mean*type0prior + DATA_PARAMETERS.type1.x.mean*type1prior;
const OverallYMean = DATA_PARAMETERS.type0.y.mean*type0prior + DATA_PARAMETERS.type1.y.mean*type1prior;
const XYcovariance = 0; // not true if covariance
const covMatrix = math.matrix([
[XVariance, XYcovariance],
[XYcovariance, YVariance],
]);

const [v0, v1, z0, z1, logPrior0, logPrior1] = calculateIntermediateValues(
type0Mean, type1Mean, covMatrix, type0prior, type1prior);
const [beta0, beta1] = calculateBeta(v0, v1, z0, z1, logPrior0, logPrior1);

return [beta0, beta1];
}

function calculateIntermediateValues(type0Mean, type1Mean, covMatrix, type0prior, type1prior) {
// intermediate values for final formula
const logPrior0 = Math.log(type0prior);
const logPrior1 = Math.log(type1prior);
if (math.det(covMatrix) == 0) {return [math.matrix([[0],[0]]), math.matrix([[0],[0]]), math.matrix([[0]]), math.matrix([[0]]), logPrior0, logPrior1]} // covMatrix has only 1 obs, so is 0 and inverse matrix cannot be calculated. For some reason the inverse is also not possible when length = 2
const v0 = math.multiply(math.inv(covMatrix), type0Mean);
const z0 = math.multiply(-0.5, (math.multiply(math.transpose(type0Mean), math.multiply(math.inv(covMatrix), type0Mean))));
const v1 = math.multiply(math.inv(covMatrix), type1Mean);
const z1 = math.multiply(-0.5, (math.multiply(math.transpose(type1Mean), math.multiply(math.inv(covMatrix), type1Mean))));

if (!(getDimensionOfMatrix(v0) == [2,1])
&& !(getDimensionOfMatrix(v1) == [2,1])
&& !(getDimensionOfMatrix(z0) == [1,1])
&& !(getDimensionOfMatrix(z1) == [1,1])) {
new Error('Dimension of Matrix not correct :/');
}
return [v0, v1, z0, z1, logPrior0, logPrior1];
}
function calculateBeta(v0, v1, z0, z1, logPrior0, logPrior1) {
// calculate lda line coefficients
const beta0 = (((z1.get([0,0]) - z0.get([0,0]) + logPrior1 - logPrior0))/
(v0.get([1,0]) - v1.get([1,0])));
const beta1 = (v1.get([0,0]) - v0.get([0,0]))/
(v0.get([1,0]) - v1.get([1,0]));
return [beta0, beta1];
}
return {
estimatedBoundary,
bayesBoundary,
calculateIntermediateValues,
calculatePriorsSample,
calculatePriorsPopulation,
}
}

Insert cell
function calculateQDAcoefficients(data) {
const clc = calculateLineCoefficients();
function estimatedBoundary(data) {
const [type0Mean, type1Mean, covMatrix, variances, covMatrixType0, covMatrixType1] = calculateLdaEstimates(data);
const {type0prior, type1prior} = clc.calculatePriorsSample(data);
const [a0, b0, c0, d0, e0, f0] = calculateIntermediateValues(covMatrixType0,
type0Mean._data[0][0],
type0Mean._data[1][0],
variances['type0VarianceX'],
variances['type0VarianceY'],
type0prior);
const [a1, b1, c1, d1, e1, f1] = calculateIntermediateValues(covMatrixType1,
type1Mean._data[0][0],
type1Mean._data[1][0],
variances['type1VarianceX'],
variances['type1VarianceY'],
type1prior);

const a = a0 - a1;
const b = b0 - b1;
const c = c0 - c1;
const d = d0 - d1;
const e = e0 - e1;
const f = f0 - f1;

return [a, b, c, d, e, f]
}
function bayesBoundary() {
const DATA_PARAMETERS = getDataParameters();
const {type0prior, type1prior} = clc.calculatePriorsPopulation(data);

const covMatrixType0 = math.matrix([
[DATA_PARAMETERS.type0.x.variance,0],
[0,DATA_PARAMETERS.type0.y.variance]
]);

const covMatrixType1 = math.matrix([
[DATA_PARAMETERS.type1.x.variance,0],
[0,DATA_PARAMETERS.type1.y.variance]
])
const [a0, b0, c0, d0, e0, f0] = calculateIntermediateValues(covMatrixType0,
DATA_PARAMETERS.type0.x.mean,
DATA_PARAMETERS.type0.y.mean,
DATA_PARAMETERS.type0.x.variance,
DATA_PARAMETERS.type0.y.variance,
type0prior);
const [a1, b1, c1, d1, e1, f1] = calculateIntermediateValues(covMatrixType1,
DATA_PARAMETERS.type1.x.mean,
DATA_PARAMETERS.type1.y.mean,
DATA_PARAMETERS.type1.x.variance,
DATA_PARAMETERS.type1.y.variance,
type1prior);

const a = a0 - a1;
const b = b0 - b1;
const c = c0 - c1;
const d = d0 - d1;
const e = e0 - e1;
const f = f0 - f1;
return [a, b, c, d, e, f]
}

function calculateIntermediateValues(covMatrix, xMean, yMean, xVar, yVar, prior) {
if (math.det(covMatrix) == 0) {return [0, 0, 0, 0, 0, 0]}
const det = math.det(covMatrix);
const meanVector = math.matrix([[xMean],[yMean]]);
const a = -0.5*yVar/det;
const b = (yVar*xMean - covMatrix._data[1][0]*yMean)/det;
const c = covMatrix._data[1][0]/det;
const d = (-covMatrix._data[1][0]*xMean + xVar*yMean)/-det;
const e = -0.5*xVar/det;
const f = -0.5*math.multiply(math.transpose(meanVector),
math.multiply(math.inv(covMatrix), meanVector))._data[0][0]
- 0.5 * Math.log(det) + Math.log(prior);
return [a, b, c, d, e, f]
}
return {
estimatedBoundary,
bayesBoundary,
calculateIntermediateValues,
}
}
Insert cell
function calculateLdaEstimates(data){
// calculate all metrics from data for further processing
const numberOfTypes = 2; // might change that in the future
const type0Data = data.filter((d) => d.type === 0);
const type1Data = data.filter((d) => d.type === 1);
const type0prior = type0Data.length/(data.length);
const type1prior = type1Data.length/(data.length);
// Mean and variance for type 0 x-coordinates
const type0MeanX = type0Data.reduce((acc, val) => acc + val.xCoord, 0) / type0Data.length;
const type0VarianceX = type0Data.reduce((acc, val) => acc + (val.xCoord - type0MeanX) ** 2, 0) / type0Data.length;
// Mean and variance for type 0 y-coordinates
const type0MeanY = type0Data.reduce((acc, val) => acc + val.yCoord, 0) / type0Data.length;
const type0VarianceY = type0Data.reduce((acc, val) => acc + (val.yCoord - type0MeanY) ** 2, 0) / type0Data.length;
// Mean and variance for type 1 x-coordinates
const type1MeanX = type1Data.reduce((acc, val) => acc + val.xCoord, 0) / type1Data.length;
const type1VarianceX = type1Data.reduce((acc, val) => acc + (val.xCoord - type1MeanX) ** 2, 0) / type1Data.length;
// Mean and variance for type 1 y-coordinates
const type1MeanY = type1Data.reduce((acc, val) => acc + val.yCoord, 0) / type1Data.length;
const type1VarianceY = type1Data.reduce((acc, val) => acc + (val.yCoord - type1MeanY) ** 2, 0) / type1Data.length;

// Common variances across classes
// not the same as variance disregarding types (OverallVariance)
// not the same as class-specific variances (type0/1Variance)
const XVariance = (type0Data.reduce((acc, val) => acc + (val.xCoord - type0MeanX) ** 2, 0) + type1Data.reduce((acc, val) => acc + (val.xCoord - type1MeanX) ** 2, 0)) / (data.length - numberOfTypes);
const YVariance = (type0Data.reduce((acc, val) => acc + (val.yCoord - type0MeanY) ** 2, 0) + type1Data.reduce((acc, val) => acc + (val.yCoord - type1MeanY) ** 2, 0)) / (data.length - numberOfTypes);
const XYcovariance = (type0Data.reduce((acc, val) => acc + (val.yCoord - type0MeanY) * (val.xCoord - type0MeanX), 0) + type1Data.reduce((acc, val) => acc + (val.yCoord - type1MeanY) * (val.xCoord - type1MeanX), 0)) / (data.length - numberOfTypes);
// Overall covariance for LDA
// Calculated with the mean disregarding types
const OverallXMean = d3.mean(data, d => d.xCoord);
const OverallYMean = d3.mean(data, d => d.yCoord);
const OverallXYcovariance = data.reduce((acc, val) => acc + (val.xCoord - OverallXMean) * (val.yCoord- OverallYMean), 0) / (data.length - 0);
// Calculate correlation just for fun
// not the same as OverallXVariance = d3.variance(data, d => d[0]);
const OverallXVariance = (data.reduce((acc, val) => acc + (val.xCoord - OverallXMean) ** 2, 0)) / (data.length);
// not the same as YVarianceTotal = d3.variance(data, d => d[1]);
const OverallYVariance = (data.reduce((acc, val) => acc + (val.yCoord - OverallYMean) ** 2, 0)) / (data.length);
const OverallXYcorrelation = OverallXYcovariance/(OverallXVariance**0.5*OverallYVariance**0.5);
d3.select("#cov").text(OverallXYcovariance);
d3.select("#cor").text(OverallXYcorrelation);
// Class-specific covariance for QDA
const covarianceType0 = type0Data.reduce((acc, val) => acc + (val.xCoord - type0MeanX) * (val.yCoord- type0MeanY), 0) / (type0Data.length - 0);
const covarianceType1 = type1Data.reduce((acc, val) => acc + (val.xCoord - type1MeanX) * (val.yCoord- type1MeanY), 0) / (type1Data.length - 0);
// covariance matrix
const covMatrix = math.matrix ([
[XVariance, XYcovariance],
[XYcovariance, YVariance],
]);
const type0Mean = math.matrix([
[type0MeanX],
[type0MeanY],
]);
const type1Mean = math.matrix([
[type1MeanX],
[type1MeanY],
]);

// class-specific covariance matrices
const covMatrixType0 = math.matrix ([
[type0VarianceX, covarianceType0],
[covarianceType0, type0VarianceY],
]);
const covMatrixType1 = math.matrix ([
[type1VarianceX, covarianceType1],
[covarianceType1, type1VarianceY],
]);

// variances for drawing
const variances = {type0VarianceX, type0VarianceY, type1VarianceX, type1VarianceY, XVariance, YVariance, OverallXVariance, OverallYVariance};
return [type0Mean, type1Mean, covMatrix, variances, covMatrixType0, covMatrixType1];

}

Insert cell
function calculateLdaDelta(x, y, data) {
const clc = calculateLineCoefficients(data);
const {type0prior, type1prior} = clc.calculatePriorsSample(data);
const [type0Mean, type1Mean, covMatrix, variances, covMatrixType0, covMatrixType1] = calculateLdaEstimates(data);
const [v0, v1, z0, z1, logPrior0, logPrior1] = clc.calculateIntermediateValues(type0Mean, type1Mean, covMatrix, type0prior, type1prior);
const mouseCoords = math.matrix([
[x],
[y],
]);

const LDAdelta0 = math.round((math.multiply(math.transpose(mouseCoords), v0)._data[0][0] + z0._data[0][0] + logPrior0)*100)/100;
const LDAdelta1 = math.round((math.multiply(math.transpose(mouseCoords), v1)._data[0][0] + z1._data[0][0] + logPrior1)*100)/100;

d3.select("#delta0").text(LDAdelta0);
d3.select("#delta1").text(LDAdelta1);
};
Insert cell
function drawDecisionBoundary(data, svg) {
let clc = calculateLineCoefficients();
const [beta0Estimated, beta1Estimated] = clc.estimatedBoundary(data);
const optionsEstimated = {
domId: 'ldalineEstimated',
stroke: "0, 0",
strokewidth: 2,
displayBeta0: "beta0Estimated",
displayBeta1: "beta1Estimated",
};
drawBoundary(beta0Estimated, beta1Estimated, optionsEstimated);
const [beta0Bayes, beta1Bayes] = clc.bayesBoundary();
const optionsBayes = {
domId: 'ldalineBayes',
stroke: "3, 3",
strokewidth: 4,
displayBeta0: "beta0Bayes",
displayBeta1: "beta1Bayes",
};
drawBoundary(beta0Bayes, beta1Bayes, optionsBayes);
function drawBoundary(beta0, beta1, options) {
const curve = d3.line().curve(d3.curveLinear);
let points = [
[cfg.xScale(0), cfg.yScale(beta0)],
[cfg.xScale(cfg.xDomain[1]), cfg.yScale(beta0 + cfg.xDomain[1]*beta1)]
];
d3.select("svg").select('#' + options['domId']).remove(); // #* add transition here
svg
.append('path')
.attr('id', options['domId'])
.attr('d', curve(points))
.attr('stroke', 'black') // d3.schemeSet3[8]
.attr("stroke-width", options['strokewidth'])
.style("stroke-dasharray", options['stroke'])
.attr('fill', 'none')
.attr('display', 'block');
d3.select("#" + options['displayBeta0']).text(beta0);
d3.select("#" + options['displayBeta1']).text(beta1);
}
}
Insert cell
function drawQDADecisionBoundary(data, svg) {
const cqc = calculateQDAcoefficients();
const [a, b, c, d, e, f] = cqc.estimatedBoundary(data);
const [aBayes, bBayes, cBayes, dBayes, eBayes, fBayes] = cqc.bayesBoundary(data);
const optionsEstimated = {
domId: 'qdalineEstimated',
stroke: "0, 0",
};
const optionsBayes = {
domId: 'qdalineBayes',
stroke: "3, 3",
};
//drawBoundary(a, b, c, d, e, f, optionsEstimated);
//drawBoundary(aBayes, bBayes, cBayes, dBayes, eBayes, fBayes, optionsBayes);
function drawBoundary(a, b, c, d, e, f, options) {
// create data points
// two lines because we have up to two solutions for each x
let QDAcurvePoints1 = [{x: null, y: null}];
let QDAcurvePoints2 = [{x: null, y: null}];
QDAcurvePoints1.pop();
QDAcurvePoints2.pop();
let y1, y2;
console.log(Math.round(100*a)/100, Math.round(100*b)/100, Math.round(100*c)/100, Math.round(100*d)/100, Math.round(100*e)/100, Math.round(100*f)/100);
d3.ticks(cfg.xDomain[0], cfg.xDomain[1], 45).forEach((x) => {
// solve for y:
const p = (c*x+d)/e;
const q = (a*x**2+b*x+f)/e;
if ((0.5*p)**2-q >= 0) {
y1 = -0.5*p + ((0.5*p)**2-q)**0.5;
QDAcurvePoints1.push({x: x, y: y1});
}
});
d3.ticks(cfg.xDomain[0], cfg.xDomain[1], 45).forEach((x) => {
// solve for y:
const p = (c*x+d)/e;
const q = (a*x**2+b*x+f)/e;
if ((0.5*p)**2-q >= 0) {
y2 = -0.5*p - ((0.5*p)**2-q)**0.5;
QDAcurvePoints2.push({x: x, y: y2});
}
});
// plot
// Create the line generator
let line = d3.line()
.x((d) => cfg.xScale(d.x))
.y((d) => cfg.yScale(d.y))
// remove old lines
d3.selectAll("*").filter(function() {
return d3.select(this).attr("name") === options['domId']+"1" ||
d3.select(this).attr("name") === options['domId']+"2";
}).remove();
// draw first line
const QDAcurve1 = svg
.append("path")
.attr('name', options['domId'] + "1")
.attr("d", line(QDAcurvePoints1))
.attr("stroke", "grey")
.attr("stroke-width", 2)
.style("stroke-dasharray", options['stroke'])
.attr("fill", "none")
;
// draw second line
const QDAcurve2 = svg
.append("path")
.attr('name', options['domId'] + "2")
.attr("d", line(QDAcurvePoints2))
.attr("stroke", "grey")
.attr("stroke-width", 2)
.style("stroke-dasharray", options['stroke'])
.attr("fill", "none")
;

}
}
Insert cell
function drawMeans(data, meansSvg) {
let [type0Mean, type1Mean, ] = calculateLdaEstimates(data)

d3.selectAll("*").filter(function() {
return d3.select(this).attr("name") === "meanLine";
}).remove();
const curve1 = d3.line().curve(d3.curveLinear);
let type0MeanXLine = [
[cfg.xScale(type0Mean.get([0,0])), cfg.yScale(cfg.yDomain[0])],
[cfg.xScale(type0Mean.get([0,0])), cfg.yScale(cfg.yDomain[1])]
];

meansSvg
.append('path')
.attr('name', 'meanLine')
.attr('d', curve1(type0MeanXLine))
.attr('stroke', d3.schemeSet3[3])
.attr("stroke-width", 2)
.attr('fill', 'none')
.style("stroke-dasharray", ("3, 3"));


const curve2 = d3.line().curve(d3.curveLinear);
let type1MeanXLine = [
[cfg.xScale(type1Mean.get([0,0])), cfg.yScale(cfg.yDomain[0])],
[cfg.xScale(type1Mean.get([0,0])), cfg.yScale(cfg.yDomain[1])]
];

meansSvg
.append('path')
.attr('name', 'meanLine')
.attr('d', curve2(type1MeanXLine))
.attr('stroke', d3.schemeSet3[4])
.attr("stroke-width", 2)
.attr('fill', 'none')
.style("stroke-dasharray", ("3, 3"));

const curve3 = d3.line().curve(d3.curveLinear);
let type0MeanYLine = [
[cfg.xScale(cfg.xDomain[0]), cfg.yScale(type0Mean.get([1,0]))],
[cfg.xScale(cfg.xDomain[1]), cfg.yScale(type0Mean.get([1,0]))]
];

meansSvg
.append('path')
.attr('name', 'meanLine')
.attr('d', curve3(type0MeanYLine))
.attr('stroke', d3.schemeSet3[3])
.attr("stroke-width", 2)
.attr('fill', 'none')
.style("stroke-dasharray", ("3, 3"));


const curve4 = d3.line().curve(d3.curveLinear);
let type1MeanYLine = [
[cfg.xScale(cfg.xDomain[0]), cfg.yScale(type1Mean.get([1,0]))],
[cfg.xScale(cfg.xDomain[1]), cfg.yScale(type1Mean.get([1,0]))]
];

meansSvg
.append('path')
.attr('name', 'meanLine')
.attr('d', curve4(type1MeanYLine))
.attr('stroke', d3.schemeSet3[4])
.attr("stroke-width", 2)
.attr('fill', 'none')
.style("stroke-dasharray", ("3, 3"));
}
Insert cell
function getDimensionOfMatrix (matrix) {
let dimensions = [
matrix._data.length,
matrix._data.reduce((x, y) => Math.max(x, y.length), 0)
];
return dimensions
}
Insert cell
function updateVariances(data, varianceRectType0X) {
const [type0Mean, type1Mean, , variances] = calculateLdaEstimates(data); // #* this should probably be passed as arguments from buildPLot instead of being calculated once again here

const meansAndVariances = [
{
id: 'type0X',
type: 0,
axis: 'x',
mean: type0Mean._data[0][0],
variance: variances.type0VarianceX,
},
{
id: 'type0Y',
type: 0,
axis: 'y',
mean: type0Mean._data[1][0],
variance: variances.type0VarianceY,
},
{
id: 'type1X',
type: 1,
axis: 'x',
mean: type1Mean._data[0][0],
variance: variances.type1VarianceX,
},
{
id: 'type1Y',
type: 1,
axis: 'y',
mean: type1Mean._data[1][0],
variance: variances.type1VarianceY,
}
]
return varianceRectType0X
.data(meansAndVariances, (d) => d.id)
.join(
(enter) =>
enter
.append('rect')
.attr('name', 'varianceDisplay')
.attr('x', (d) => d.axis == 'x' ? cfg.xScale(d.mean - Math.abs(d.variance)) : 0)
.attr('y', (d) => d.axis == 'x' ? 0 : cfg.yScale(d.mean + Math.abs(d.variance)))
.attr('width', (d) => d.axis == 'x' ? cfg.xScale(Math.abs(d.variance*2))
- cfg.insetLeft -cfg.marginLeft : cfg.width)
.attr('height', (d) => d.axis == 'x' ? cfg.height :
Math.abs(cfg.yScale(Math.abs(d.variance*2))
- cfg.height + cfg.marginTop + cfg.insetTop))
.attr('fill', (d) => d.type == 0 ? d3.schemeSet3[3] : d3.schemeSet3[8])
.attr('opacity', 0.29)
,
(update) =>
update
.attr('x', (d) => d.axis == 'x' ? cfg.xScale(d.mean - Math.abs(d.variance)) : 0)
.attr('y', (d) => d.axis == 'x' ? 0 : cfg.yScale(d.mean + Math.abs(d.variance)))
.attr('width', (d) => d.axis == 'x' ? cfg.xScale(Math.abs(d.variance*2))
- cfg.insetLeft - cfg.marginLeft : cfg.width)
.attr('height', (d) => d.axis == 'x' ? cfg.height :
Math.abs(cfg.yScale(Math.abs(d.variance*2))
- cfg.height + cfg.marginTop + cfg.insetTop))
,
(exit) => exit.remove() // actually not relevant for variances
);
}
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