function buildPlot(data) {
let idCounter = 0;
const accesX = ([x]) => x;
const accesY = ([, y]) => y;
const accesType = ([, , type]) => type;
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();
const initData = data.map((d) => ({ ...d }));
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
});
}