I’ve created two charts using Javascript and D3 and I’m trying to position them side by side (horizontally). To achieve this I’m using CSS Grid and I’ve housed both charts (#mainChart) and (#barChart) inside a container class “.grid-container”. Whilst barChart is displayed on the right hand side of the container (as expected), mainChart is displayed underneath the container (on the left side). I’m struggling to position mainChart inside the container (still on the left side). I’ve tried using Flexbox and got the exact same problem, which tells me it’s not the CSS tool that is the problem.
<!DOCTYPE html>
<html>
<head>
<script src="https://d3js.org/d3.v7.min.js"></script>
<link href="https://fonts.googleapis.com/css2?family=Lato:wght@400;700&display=swap" rel="stylesheet">
<style>
body {
max-width: 1000px; /* Set the maximum width of the page */
margin: 0 auto; /* Center the page horizontally */
padding: 20px; /* Add padding to the content */
background-color: lightblue;
}
svg {
font-family: "Lato", sans-serif;
}
.grid-container {
display: grid;
grid-template-columns: 1fr 1fr; /* Two columns with equal width */
border: 5px solid blue;
}
#mainChart {
border: 5px solid green;
}
#barChart {
border: 5px solid rgb(255, 0, 0);
}
</style>
</head>
<body>
<div class="grid-container">
<div id="mainChart"></div>
<div id="barChart"></div>
</div>
<script src="main.js"></script>
<script src="year_bar.js"></script>
</body>
</html>
main.js (JS/D3 for creating the mainChart)
// Define SVG dimensions and margins
const svgWidth = 500;
const svgHeight = 130;
const margin = { top: 70, right: 10, bottom: 30, left: 250 };
const height = svgHeight - margin.top - margin.bottom;
const width = svgWidth - margin.left - margin.right;
// Add the SVG to the body
let svg = d3
.select("body")
.append("svg")
.attr("width", svgWidth)
.attr("height", svgHeight * 5)
.attr("id", "mainChart")
// Initialize the selected label index
let selectedLabelIndex = -1;
function createStrandKDEPlot(dataFileName, index, color, label, opacity) {
// Load data from CSV
d3.csv(dataFileName).then((data) => {
// Convert strings to numbers
data.forEach((d) => {
d.effect_size = +d.effect_size;
d.frequency = +d.frequency;
});
// Set initial data points to zero
const initialData = data.map((d) => ({
effect_size: d.effect_size,
frequency: 0,
}));
const xScale = d3.scaleLinear().domain([-4, 4]).range([0, width]);
const yScale = d3.scaleLinear().domain([0, d3.max(data, (d) => d.frequency)]).range([height, 0]);
// Define the line for the initial flat curve
const initialLine = d3
.line()
.curve(d3.curveBasis)
.x((d) => xScale(d.effect_size))
.y((d) => yScale(0)); // Change y-value to 0 for initial flat curve
// Define the line for the animated curve
const animatedLine = d3
.line()
.curve(d3.curveBasis)
.x((d) => xScale(d.effect_size))
.y((d) => yScale(d.frequency));
// Define the area under the curve
const area = d3
.area()
.curve(d3.curveBasis)
.x((d) => xScale(d.effect_size))
.y1((d) => yScale(d.frequency))
.y0(yScale(0));
// Add a group for each plot
const g = svg
.append("g")
.attr(
"transform",
`translate(${margin.left - 30}, ${index * (svgHeight - 110) + margin.top})`
);
// Draw the curve border line
g.append("path")
.datum(data)
.attr("fill", "none")
.attr("stroke", "#5A5A5A")
.attr("stroke-width", 1)
.attr("d", initialLine);
// Define the area under the curve for the initial flat curve
const initialArea = d3
.area()
.curve(d3.curveBasis)
.x((d) => xScale(d.effect_size))
.y1((d) => yScale(0))
.y0(yScale(0));
// Add the initial flat curve area
g.append("path")
.datum(initialData)
.attr("fill", "none") // Set fill to none
.attr("d", initialArea);
// Transition the area under the curve
g.select("path")
.transition()
.duration(500) // Set the duration of the transition
.attr("fill", color) // Set the fill color
.attr("d", area);
// Draw the area under the curve with the specified color and opacity
g.append("path")
.datum(initialData) // Use initialData instead of data for initial flat curve
.style("fill", color)
.style("opacity", opacity) // Set the opacity
.attr("stroke", color) // Set the stroke color
.attr("d", area);
// Animate the curve and label after a time delay
setTimeout(() => {
g.selectAll("path")
.transition()
.duration(1000) // Set the duration of the transition
.ease(d3.easeQuad) // Set the easing function for the transition
.attr("d", animatedLine); // Transition the curve from initial flat curve to the actual curve
textLabel.transition().duration(250).attr("opacity", 1);
circle.transition().duration(250).attr("opacity", 1);
}, index * 0); // Adjust the delay duration as per your preference
// Add the text label
const textLabel = g
.append("text")
.attr("x", -margin.left / 12)
.attr("y", height / 1)
.attr("text-anchor", "end")
.attr("alignment-baseline", "middle")
.style("font-size", "12px")
.attr("opacity", 0)
.text(label)
.classed("text-label", true); // Add class for text label
const circle = g
.append("circle")
.attr("cx", -margin.left / 25) // Adjust the position of the circle
.attr("cy", height / 1)
.attr("r", 4) // Set the radius of the circle
.style("fill", color)
.attr("opacity", 0);
// Draw the curve border line
g.append("path")
.datum(data)
.attr("fill", "none")
.attr("stroke", "#5A5A5A")
.attr("stroke-width", 1)
.attr("d", initialLine)
.classed("curve-outline", true) // Add class for curve outline
.attr("id", `curve-outline-${index}`); // Add id for each curve outline
// Get the bounding box of the text label
const labelBBox = textLabel.node().getBBox();
// Add a background rectangle behind the text label
const labelBackground = g
.insert("rect", "text") // Insert before the text label
.attr("x", labelBBox.x - 4)
.attr("y", labelBBox.y - 2)
.attr("width", labelBBox.width + 8)
.attr("height", labelBBox.height + 4)
.attr("rx", 2)
.attr("ry", 2)
.style("fill", "none")
.classed("label-background", true); // Add a class for the background rectangle
// Add event listeners for hover and click
textLabel.on("mouseenter", function () {
if (index !== selectedLabelIndex) {
labelBackground.style("fill", "black");
textLabel.style("fill", "white");
g.select("path")
.style("stroke", "#5A5A5A") // Set the border color of the curve to black
.style("stroke-width", 3);
svg.style("cursor", "pointer"); // Change the cursor style to "pointer"
}
});
textLabel.on("mouseleave", function () {
if (index !== selectedLabelIndex) {
labelBackground.style("fill", "none");
textLabel.style("fill", "black");
g.select("path")
.style("stroke", "#5A5A5A") // Revert the border color of the curve to the original color
.style("stroke-width", 1);
}
svg.style("cursor", "auto"); // Revert the cursor style to the default value
});
let selectedCurveOutlines = {};
textLabel.on("click", function () {
if (selectedLabelIndex !== index) {
// Remove black background from previously selected label
svg.selectAll(".label-background").style("fill", "none");
svg.selectAll(".text-label").style("fill", "black");
// Revert the previously selected curve outline to default stroke width
if (selectedCurveOutlines[selectedLabelIndex]) {
selectedCurveOutlines[selectedLabelIndex].style("stroke-width", 1);
}
// Set black background for the clicked label
labelBackground.style("fill", "black");
textLabel.style("fill", "white");
// Set the border color and width of the curve outline for the clicked label
const curveOutline = g.select("path");
curveOutline
.style("stroke", "#5A5A5A") // Set the border color of the curve to black
.style("stroke-width", 1);
// Update the selected label index and curve outline
selectedLabelIndex = index;
selectedCurveOutlines[selectedLabelIndex] = curveOutline;
if (selectedLabelIndex === 0) {
// Load new data from CSV (replace "ry_year_bar.csv" with the path to your new data file)
d3.csv("ry_year_bar.csv").then(yearBarData => {
// Update the existing chart with the new data
const container = document.getElementById("barChart");
container.innerHTML = ""; // Clear the chart container
const chart = createYearBarChart(yearBarData, "steelblue", 13); // Manually set the maximum value to 13
container.appendChild(chart.svg.node());
});
} else if (selectedLabelIndex === 1) {
// Load data from "sets.csv"
d3.csv("sets_year_bar.csv").then(setsData => {
// Update the existing chart with the sets data
const container = document.getElementById("barChart");
container.innerHTML = ""; // Clear the chart container
const chart = createYearBarChart(setsData, "steelblue", 30);
container.appendChild(chart.svg.node());
});
} else if (selectedLabelIndex === 2) {
// Load data from "sets.csv"
d3.csv("rc_year_bar.csv").then(setsData => {
// Update the existing chart with the sets data
const container = document.getElementById("barChart");
container.innerHTML = ""; // Clear the chart container
const chart = createYearBarChart(setsData, "steelblue", 50);
container.appendChild(chart.svg.node());
});
} else {
// Load the default data from CSV (replace "alphabet.csv" with the path to your default data file)
d3.csv("alphabet.csv").then(defaultData => {
// Update the existing chart with the default data
const container = document.getElementById("barChart");
container.innerHTML = ""; // Clear the chart container
const chart = createYearBarChart(defaultData, "steelblue", 42);
container.appendChild(chart.svg.node());
});
}
}
});
labelBackground.on("click", function () {
if (selectedLabelIndex !== index) {
// Remove black background from previously selected label
svg.selectAll(".label-background").style("fill", "none");
// Set black background for the clicked label
labelBackground.style("fill", "black");
// Update the selected label index
selectedLabelIndex = index;
// Rest of your code...
}
});
// Add the x-axis line with ticks and numbers
const xAxis = d3.axisBottom(xScale);
const xAxisGroup = svg
.append("g")
.attr(
"transform",
`translate(${margin.left - 20}, ${svgHeight - margin.bottom + 480})`
) // Adjust the y-coordinate value here
.call(xAxis);
// Modify the stroke color of the x-axis line
xAxisGroup.select(".domain").attr("stroke", "#5A5A5A");
});
}
// Array of data file names
let dataFileNames = [
"ry.csv",
"ss.csv",
"rc.csv",
"m.csv",
"pp.csv",
"ap.csv",
"pa.csv",
"sgt.csv",
"ii.csv",
"bi.csv",
"est.csv",
"ss.csv",
"fb.csv",
"ol.csv",
"pt.csv",
"sel.csv",
"pe.csv",
"ml.csv",
"cl.csv",
"ph.csv",
"hw.csv",
"one_one.csv",
"ta.csv",
"rc.csv",
"msr.csv",
];
let colors = [
"#ff0000", // Red
"#ff3300",
"#ff6600",
"#ff9900",
"#ffcc00",
"#ffef00",
"#ccff00", // Yellow-Green
"#99ff00",
"#66ff00",
"#33ff00",
"#00ff00", // Green
"#00ff33",
"#00ff66",
"#00ff99",
"#00ffcc",
"#00ffff", // Cyan
"#00ccff",
"#0099ff",
"#0066ff",
"#0033ff",
"#0000ff", // Blue
"#3300ff",
"#6600ff",
"#9900ff",
"#cc00ff",
"#ff00ff", // Magenta
];
let labels = [
"Repeating a Year",
"Setting/Streaming",
"Reading Comprehension",
"Mentoring",
"Performance Pay",
"Arts Participation",
"Physical Activity",
"Small Group Tuition",
"Individualised Instruction",
"Behavior Interventions",
"Earlier Starting Time",
"Summer Schools",
"Feedback",
"Oral Language",
"Peer Tutoring",
"Social & Emotional Learning",
"Parental Engagement",
"Mastery Learning",
"Collaborative Learning",
"Phonics",
"Homework",
"One to One",
"Teaching Assistants",
"Reading Comprehension",
"Metacognition & Self-Regulation",
];
// Define x and y scales
const xScale = d3.scaleLinear().domain([-4, 4]).range([0, width]);
const yScale = d3.scaleLinear().domain([0, 1]).range([height, 0]);
// Initialize a delay counter
let delay = 0;
// Reverse the dataFileNames array to start with the first plot at the top
dataFileNames.reverse().forEach((fileName, index) => {
const opacity = 0.8; // Set the desired opacity value
// Use setTimeout to introduce a time delay between each plot rendering
setTimeout(() => {
createStrandKDEPlot(fileName, index, colors[index], labels[index], opacity);
}, delay);
// Increment the delay counter
delay += 50; // Adjust the delay duration as per your preference
});
year_bar.js (JS/D3 for creating barChart)
function createYearBarChart(data, color, maxYValue) {
// Specify the chart’s dimensions.
const width = 500;
const height = 300;
const marginTop = 20;
const marginRight = 20;
const marginBottom = 30;
const marginLeft = 40;
// Declare the x (horizontal position) scale and the corresponding axis generator.
const x = d3
.scaleBand()
.domain(data.map((d) => d.letter))
.range([marginLeft, width - marginRight])
.paddingInner(0.2)
.paddingOuter(0.2);
const xAxis = d3.axisBottom(x).tickSizeOuter(0);
// Declare the y (vertical position) scale.
const y = d3.scaleLinear().range([height - marginBottom, marginTop]);
// Adjust the y-axis tick format
const yAxis = d3.axisLeft(y).tickFormat((y) => y.toFixed());
// Create the SVG container.
const svg = d3
.create("svg")
.attr("viewBox", [0, 0, width, height])
.style("background-color", "green");
// Create a group for the bars.
const barsGroup = svg.append("g");
// Create the axes.
const gx = svg
.append("g")
.attr("transform", `translate(0,${height - marginBottom})`)
.call(xAxis)
.selectAll("text")
.attr("transform", "rotate(-45)")
.style("text-anchor", "end")
.attr("dx", "-0.5em")
.attr("dy", "0.5em");
const gy = svg.append("g").attr("transform", `translate(${marginLeft},0)`).call(yAxis);
// Function to update the chart with new data
function update(newData) {
// Update the domain of the y-scale with the new data
// Update the domain of the y-scale with the fixed maxYValue
y.domain([0, maxYValue]).nice();
const bars = barsGroup.selectAll("rect").data(newData);
bars
.enter()
.append("rect")
.attr("x", (d) => x(d.letter))
.attr("width", x.bandwidth())
.attr("y", y(0))
.attr("height", 0)
.attr("fill", color)
.merge(bars)
.transition()
.duration(500)
.attr("y", (d) => y(d.frequency))
.attr("height", (d) => y(0) - y(d.frequency));
bars.exit().remove();
gx.transition().duration(500).call(xAxis);
gy.transition().duration(500).call(yAxis);
// Update the SVG viewbox with the new dimensions
svg.attr("viewBox", [0, 0, width, height]);
}
// Initial chart rendering
update(data);
// Return the chart with the update method
return {
svg,
update,
};
}
// Load the initial data (alphabet.csv in this case)
d3.csv("alphabet.csv").then((data) => {
// Create the chart with the initial data and color
const yearBarChart = createYearBarChart(data, "red", 42);
// Append the chart's SVG to the barChartContainer div
const container = document.getElementById("barChart");
container.appendChild(yearBarChart.svg.node());
// Add click event listeners to the text elements
const textLabels = document.querySelectorAll(".text-label");
textLabels.forEach((label, index) => {
label.addEventListener("click", () => handleTextClick(index));
});
});
Any help is appreciated.