{
const width = 800;
const height = 500;
const margin = ({top: 20, right: 120, bottom: 60, left: 80});
const svg = d3.create("svg")
.attr("viewBox", [0, 0, width, height]);
const isCategorical = (variable) => {
return ["major", "satisfaction_level"].includes(variable);
};
const getUniqueValues = (data, variable) => {
return [...new Set(data.map(d => d[variable]))];
};
let xScale, yScale;
if (isCategorical(xAxis)) {
const xDomain = getUniqueValues(studentData, xAxis);
xScale = d3.scaleBand()
.domain(xDomain)
.range([margin.left, width - margin.right])
.padding(0.1);
} else {
xScale = d3.scaleLinear()
.domain(d3.extent(studentData, d => d[xAxis]))
.range([margin.left, width - margin.right])
.nice();
}
// Y-axis scale
if (isCategorical(yAxis)) {
const yDomain = getUniqueValues(studentData, yAxis);
yScale = d3.scaleBand()
.domain(yDomain)
.range([height - margin.bottom, margin.top])
.padding(0.1);
} else {
yScale = d3.scaleLinear()
.domain(d3.extent(studentData, d => d[yAxis]))
.range([height - margin.bottom, margin.top])
.nice();
}
// Color scale
let colorScale;
if (colorEncoding !== "none") {
if (isCategorical(colorEncoding)) {
const colorDomain = getUniqueValues(studentData, colorEncoding);
colorScale = d3.scaleOrdinal()
.domain(colorDomain)
.range(d3.schemeCategory10);
} else {
colorScale = d3.scaleSequential()
.domain(d3.extent(studentData, d => d[colorEncoding]))
.interpolator(d3.interpolateViridis);
}
}
// Size scale
let sizeScale;
if (sizeEncoding !== "none") {
sizeScale = d3.scaleLinear()
.domain(d3.extent(studentData, d => d[sizeEncoding]))
.range([4, 12]);
}
// Add axes
const xAxisGenerator = isCategorical(xAxis) ? d3.axisBottom(xScale) : d3.axisBottom(xScale);
const yAxisGenerator = isCategorical(yAxis) ? d3.axisLeft(yScale) : d3.axisLeft(yScale);
svg.append("g")
.attr("transform", `translate(0,${height - margin.bottom})`)
.call(xAxisGenerator)
.selectAll("text")
.style("text-anchor", isCategorical(xAxis) ? "middle" : "end")
.attr("dx", isCategorical(xAxis) ? "0em" : "-0.8em")
.attr("dy", isCategorical(xAxis) ? "1em" : "0.15em")
.attr("transform", isCategorical(xAxis) ? null : "rotate(-45)");
svg.append("g")
.attr("transform", `translate(${margin.left},0)`)
.call(yAxisGenerator);
// Add axis labels
svg.append("text")
.attr("transform", "rotate(-90)")
.attr("y", 20)
.attr("x", -(height / 2))
.attr("text-anchor", "middle")
.style("font-size", "12px")
.text(yAxis.replace(/_/g, " "));
svg.append("text")
.attr("transform", `translate(${width / 2}, ${height - 10})`)
.attr("text-anchor", "middle")
.style("font-size", "12px")
.text(xAxis.replace(/_/g, " "));
// Add data points
const circles = svg.selectAll("circle")
.data(studentData)
.join("circle")
.attr("cx", d => {
if (isCategorical(xAxis)) {
return xScale(d[xAxis]) + xScale.bandwidth() / 2;
} else {
return xScale(d[xAxis]);
}
})
.attr("cy", d => {
if (isCategorical(yAxis)) {
return yScale(d[yAxis]) + yScale.bandwidth() / 2;
} else {
return yScale(d[yAxis]);
}
})
.attr("r", d => sizeEncoding !== "none" ? sizeScale(d[sizeEncoding]) : 6)
.attr("fill", d => {
if (colorEncoding !== "none") {
return colorScale(d[colorEncoding]);
} else {
return "steelblue";
}
})
.attr("opacity", 0.7)
.attr("stroke", "white")
.attr("stroke-width", 1);
// Add tooltips
circles.append("title")
.text(d => `Student ${d.id}
Major: ${d.major}
GPA: ${d.gpa}
Credits: ${d.credits_completed}
Graduation: ${d.graduation_year}
Satisfaction: ${d.satisfaction_level}
Study Hours/Week: ${d.study_hours_per_week}`);
// Add legend for color encoding if applicable
if (colorEncoding !== "none") {
const legend = svg.append("g")
.attr("transform", `translate(${width - 100}, 30)`);
if (isCategorical(colorEncoding)) {
// Categorical legend (existing code)
const colorDomain = getUniqueValues(studentData, colorEncoding);
legend.selectAll("rect")
.data(colorDomain)
.join("rect")
.attr("x", 0)
.attr("y", (d, i) => i * 20)
.attr("width", 15)
.attr("height", 15)
.attr("fill", d => colorScale(d));
legend.selectAll("text")
.data(colorDomain)
.join("text")
.attr("x", 20)
.attr("y", (d, i) => i * 20 + 12)
.style("font-size", "12px")
.text(d => d);
legend.append("text")
.attr("x", 0)
.attr("y", -5)
.style("font-size", "12px")
.style("font-weight", "bold")
.text(colorEncoding.replace(/_/g, " "));
} else {
// Numerical legend (new code)
const [minVal, maxVal] = d3.extent(studentData, d => d[colorEncoding]);
const legendHeight = 100;
// Create gradient definition
const defs = svg.append("defs");
const gradient = defs.append("linearGradient")
.attr("id", "color-legend-gradient")
.attr("x1", "0%")
.attr("y1", "100%")
.attr("x2", "0%")
.attr("y2", "0%");
// Add color stops to gradient
const numStops = 10;
for (let i = 0; i <= numStops; i++) {
const value = minVal + (maxVal - minVal) * (i / numStops);
gradient.append("stop")
.attr("offset", `${(i / numStops) * 100}%`)
.attr("stop-color", colorScale(value));
}
// Add gradient rectangle
legend.append("rect")
.attr("x", 0)
.attr("y", 0)
.attr("width", 20)
.attr("height", legendHeight)
.style("fill", "url(#color-legend-gradient)")
.style("stroke", "#000")
.style("stroke-width", 1);
// Add legend labels
legend.append("text")
.attr("x", 25)
.attr("y", 5)
.style("font-size", "10px")
.text(maxVal.toFixed(1));
legend.append("text")
.attr("x", 25)
.attr("y", legendHeight + 5)
.style("font-size", "10px")
.text(minVal.toFixed(1));
// Add legend title
legend.append("text")
.attr("x", 0)
.attr("y", -5)
.style("font-size", "12px")
.style("font-weight", "bold")
.text(colorEncoding.replace(/_/g, " "));
}
}
return svg.node();
}