// Visualization with integrated alpha slider in sidebar
viewof vizWithControls = {
// Main container
const container = document.createElement("div");
container.style.display = "flex";
container.style.width = "100%";
container.style.justifyContent = "space-between";
container.style.alignItems = "flex-start";
// Theme colors from SCSS
const themeColors = {
background: "#ffffff",
foreground: "#222222",
accent: "#FF3E00",
lightGray: "#e5e5e5",
mediumGray: "#9e9e9e",
darkGray: "#555555"
};
// Plot area
const plotArea = document.createElement("div");
plotArea.style.width = "80%";
// Control panel
const controlPanel = document.createElement("div");
controlPanel.style.width = "20%";
controlPanel.style.padding = "10px";
controlPanel.style.backgroundColor = themeColors.lightGray;
controlPanel.style.borderRadius = "4px";
controlPanel.style.fontSize = "0.9em";
controlPanel.style.color = themeColors.foreground;
controlPanel.style.fontFamily = "Inter, -apple-system, BlinkMacSystemFont, 'SF Pro Text', 'Helvetica Neue', Helvetica, Arial, sans-serif";
// Create alpha slider directly in the control panel
const sliderContainer = document.createElement("div");
sliderContainer.style.marginBottom = "15px";
const sliderLabel = document.createElement("label");
sliderLabel.textContent = "α: ";
sliderLabel.htmlFor = "alpha-slider";
sliderLabel.style.marginRight = "8px";
sliderLabel.style.fontWeight = "500";
sliderLabel.style.color = themeColors.foreground;
const slider = document.createElement("input");
slider.type = "range";
slider.id = "alpha-slider";
slider.min = "0";
slider.max = "1";
slider.step = "0.01";
slider.value = 0.5; // Default value
slider.style.width = "80%";
slider.style.marginBottom = "5px";
slider.style.accentColor = themeColors.accent;
const valueDisplay = document.createElement("span");
valueDisplay.textContent = "0.50";
valueDisplay.style.marginLeft = "5px";
valueDisplay.style.fontWeight = "500";
sliderContainer.appendChild(sliderLabel);
sliderContainer.appendChild(document.createElement("br"));
sliderContainer.appendChild(slider);
sliderContainer.appendChild(valueDisplay);
// Store alpha in a variable that can be updated from our custom slider
let alphaValue = 0.5;
slider.addEventListener("input", function() {
alphaValue = parseFloat(this.value);
valueDisplay.textContent = alphaValue.toFixed(2);
updateVisualization();
container.dispatchEvent(new Event("input")); // Notify Observable of change
});
// Initial positions
let pointA = [200, 100];
let pointB = [600, 250];
// Convert between pixel coordinates and data coordinates
function toDataCoords(px, py) {
return [
xScale.invert(px),
yScale.invert(py)
];
}
function toPixelCoords(dx, dy) {
return [
xScale(dx),
yScale(dy)
];
}
const svg = d3.create("svg")
.attr("width", 700)
.attr("height", 350)
.style("font-family", "Inter, -apple-system, BlinkMacSystemFont, 'SF Pro Text', 'Helvetica Neue', Helvetica, Arial, sans-serif");
// Create scales for the axes
const xScale = d3.scaleLinear()
.domain([0, 10])
.range([40, 660]);
const yScale = d3.scaleLinear()
.domain([0, 6])
.range([310, 20]);
// Create axes
const xAxis = d3.axisBottom(xScale);
const yAxis = d3.axisLeft(yScale);
// Style the axes
svg.append("g")
.attr("transform", `translate(0,310)`)
.attr("color", themeColors.darkGray)
.call(xAxis);
svg.append("g")
.attr("transform", `translate(40,0)`)
.attr("color", themeColors.darkGray)
.call(yAxis);
// Add axis labels
svg.append("text")
.attr("x", 350)
.attr("y", 345)
.attr("text-anchor", "middle")
.attr("fill", themeColors.darkGray)
.style("font-weight", "500")
.style("font-size", "14px")
.text("X");
svg.append("text")
.attr("transform", "rotate(-90)")
.attr("x", -175)
.attr("y", 15)
.attr("text-anchor", "middle")
.attr("fill", themeColors.darkGray)
.style("font-weight", "500")
.style("font-size", "14px")
.text("Y");
// Add a subtle grid
svg.append("g")
.attr("class", "grid")
.attr("transform", `translate(0,310)`)
.call(xAxis.tickSize(-290).tickFormat(""))
.selectAll("line")
.attr("stroke", themeColors.lightGray)
.attr("stroke-opacity", 0.5);
svg.append("g")
.attr("class", "grid")
.attr("transform", `translate(40,0)`)
.call(yAxis.tickSize(-620).tickFormat(""))
.selectAll("line")
.attr("stroke", themeColors.lightGray)
.attr("stroke-opacity", 0.5);
// Remove grid border
svg.selectAll(".domain")
.attr("stroke", themeColors.darkGray);
// Data coordinates for the points
let dataA = toDataCoords(pointA[0], pointA[1]);
let dataB = toDataCoords(pointB[0], pointB[1]);
let dataZ = [
alphaValue * dataA[0] + (1 - alphaValue) * dataB[0],
alphaValue * dataA[1] + (1 - alphaValue) * dataB[1]
];
// Calculate pixel coordinates for Z
let pixelZ = toPixelCoords(dataZ[0], dataZ[1]);
// Draw line between A and B
const line = svg.append("line")
.attr("x1", pointA[0])
.attr("y1", pointA[1])
.attr("x2", pointB[0])
.attr("y2", pointB[1])
.attr("stroke", themeColors.mediumGray)
.attr("stroke-width", 1)
.attr("stroke-dasharray", "5,5");
// Point A (draggable)
const circleA = svg.append("circle")
.attr("cx", pointA[0])
.attr("cy", pointA[1])
.attr("r", 8)
.attr("fill", "#4285F4") // Google blue
.attr("stroke", themeColors.background)
.attr("stroke-width", 2)
.attr("cursor", "move")
.call(d3.drag()
.on("drag", function(event) {
pointA[0] = Math.max(40, Math.min(660, event.x));
pointA[1] = Math.max(20, Math.min(310, event.y));
dataA = toDataCoords(pointA[0], pointA[1]);
updateVisualization();
}));
// Point B (draggable)
const circleB = svg.append("circle")
.attr("cx", pointB[0])
.attr("cy", pointB[1])
.attr("r", 8)
.attr("fill", "#0F9D58") // Google green
.attr("stroke", themeColors.background)
.attr("stroke-width", 2)
.attr("cursor", "move")
.call(d3.drag()
.on("drag", function(event) {
pointB[0] = Math.max(40, Math.min(660, event.x));
pointB[1] = Math.max(20, Math.min(310, event.y));
dataB = toDataCoords(pointB[0], pointB[1]);
updateVisualization();
}));
// Point Z (convex combination)
const circleZ = svg.append("circle")
.attr("cx", pixelZ[0])
.attr("cy", pixelZ[1])
.attr("r", 8)
.attr("fill", themeColors.accent)
.attr("stroke", themeColors.background)
.attr("stroke-width", 2);
// Labels
const labelA = svg.append("text")
.attr("x", pointA[0] + 12)
.attr("y", pointA[1] - 8)
.attr("fill", themeColors.foreground)
.style("font-weight", "600")
.style("font-size", "14px")
.text("A");
const labelB = svg.append("text")
.attr("x", pointB[0] + 12)
.attr("y", pointB[1] - 8)
.attr("fill", themeColors.foreground)
.style("font-weight", "600")
.style("font-size", "14px")
.text("B");
const labelZ = svg.append("text")
.attr("x", pixelZ[0] + 12)
.attr("y", pixelZ[1] - 8)
.attr("fill", themeColors.foreground)
.style("font-weight", "600")
.style("font-size", "14px")
.text("Z");
// Update function
function updateVisualization() {
// Recalculate Z in data coordinates
dataZ = [
alphaValue * dataA[0] + (1 - alphaValue) * dataB[0],
alphaValue * dataA[1] + (1 - alphaValue) * dataB[1]
];
pixelZ = toPixelCoords(dataZ[0], dataZ[1]);
// Update line
line
.attr("x1", pointA[0])
.attr("y1", pointA[1])
.attr("x2", pointB[0])
.attr("y2", pointB[1]);
// Update circles
circleA
.attr("cx", pointA[0])
.attr("cy", pointA[1]);
circleB
.attr("cx", pointB[0])
.attr("cy", pointB[1]);
// Update point Z
circleZ
.attr("cx", pixelZ[0])
.attr("cy", pixelZ[1]);
// Update labels
labelA
.attr("x", pointA[0] + 12)
.attr("y", pointA[1] - 8);
labelB
.attr("x", pointB[0] + 12)
.attr("y", pointB[1] - 8);
labelZ
.attr("x", pixelZ[0] + 12)
.attr("y", pixelZ[1] - 8);
// Update control panel
updateControlPanel();
}
// Function to update the control panel
function updateControlPanel() {
// Update the slider value (if it wasn't the source of the update)
if (parseFloat(slider.value) !== alphaValue) {
slider.value = alphaValue;
valueDisplay.textContent = alphaValue.toFixed(2);
}
// Update the rest of the control panel info
const coordsInfo = document.createElement("div");
// Style for coordinate values
const valueStyle = `color: ${themeColors.foreground}; font-family: "SF Mono", Menlo, Consolas, Monaco, monospace; font-size: 0.9em;`;
coordsInfo.innerHTML = `
<div style="margin-bottom:10px; border-top: 1px solid ${themeColors.mediumGray}; padding-top: 8px;">
<strong style="color: #4285F4;">A:</strong> <span style="${valueStyle}">(${dataA[0].toFixed(2)}, ${dataA[1].toFixed(2)})</span>
</div>
<div style="margin-bottom:10px;">
<strong style="color: #0F9D58;">B:</strong> <span style="${valueStyle}">(${dataB[0].toFixed(2)}, ${dataB[1].toFixed(2)})</span>
</div>
<div style="margin-bottom:10px;">
<strong style="color: ${themeColors.accent};">Z:</strong> <span style="${valueStyle}">(${dataZ[0].toFixed(2)}, ${dataZ[1].toFixed(2)})</span>
</div>
<div style="margin-bottom:10px; border-top: 1px solid ${themeColors.mediumGray}; padding-top: 8px;">
<span style="color: ${themeColors.accent}; font-weight: 600;">Z</span> = <span style="color: #4285F4;">${alphaValue.toFixed(2)}A</span> + <span style="color: #0F9D58;">${(1-alphaValue).toFixed(2)}B</span>
</div>
`;
// Clear previous coordinate info and append new info
while (controlPanel.childNodes.length > 1) {
controlPanel.removeChild(controlPanel.lastChild);
}
controlPanel.appendChild(coordsInfo);
}
// Initial control panel setup
controlPanel.appendChild(sliderContainer);
updateControlPanel();
// Set up value property for Observable
Object.defineProperty(container, "value", {
get() {
return alphaValue;
}
});
// Append the SVG to the plot area
plotArea.appendChild(svg.node());
// Append everything to the container
container.appendChild(plotArea);
container.appendChild(controlPanel);
return container;
}