Social Emotional Milestones Graph
Social Emotional Milestones Tracker
<!-- Main content container -->
<div class="container-content">
<!-- Input Section -->
<div class="input-section">
<h2 class="text-2xl font-bold text-gray-800 mb-6 text-center lg:text-left">Enter Your Data Points</h2>
<div class="flex flex-wrap items-end gap-4 mb-6">
<div class="flex-1 min-w-[120px]">
<label for="ageInput" class="block text-gray-700 text-sm font-semibold mb-2">Age in Months (0-60):</label>
<input type="number" id="ageInput" min="0" max="60" value="0"
class="shadow-sm appearance-none border rounded-lg w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition duration-200 ease-in-out">
</div>
<div class="flex-1 min-w-[120px]">
<label for="milestoneInput" class="block text-gray-700 text-sm font-semibold mb-2">Social Emotional Milestone (0-6):</label>
<input type="number" id="milestoneInput" min="0" max="6" value="0"
class="shadow-sm appearance-none border rounded-lg w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition duration-200 ease-in-out">
</div>
<button id="addDataBtn"
class="flex-shrink-0 text-white font-bold py-2.5 px-4 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 transition duration-300 ease-in-out transform hover:scale-105 shadow-md">
Add Your Data Point
</button>
</div>
<div id="messageBox" class="mt-4 p-3 text-sm text-red-700 bg-red-100 border border-red-200 rounded-lg hidden" role="alert">
<!-- Error messages will be displayed here -->
</div>
<!-- New section for Baseline Data Point Input -->
<h2 class="text-2xl font-bold text-gray-800 mt-8 mb-6 text-center lg:text-left">Add Baseline Age Range for Milestone</h2>
<div class="flex flex-wrap items-end gap-4 mb-6">
<div class="flex-1 min-w-[120px]">
<label for="milestoneInputBaseline" class="block text-gray-700 text-sm font-semibold mb-2">Milestone (0-6):</label>
<input type="number" id="milestoneInputBaseline" min="0" max="6" value="0"
class="shadow-sm appearance-none border rounded-lg w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:ring-2 focus:ring-purple-500 focus:border-transparent transition duration-200 ease-in-out">
</div>
<div class="flex-1 min-w-[120px]">
<label for="ageInputBaselineLower" class="block text-gray-700 text-sm font-semibold mb-2">Lower Age (months, 0-60):</label>
<input type="number" id="ageInputBaselineLower" min="0" max="60" value="0"
class="shadow-sm appearance-none border rounded-lg w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:ring-2 focus:ring-purple-500 focus:border-transparent transition duration-200 ease-in-out">
</div>
<div class="flex-1 min-w-[120px]">
<label for="ageInputBaselineUpper" class="block text-gray-700 text-sm font-semibold mb-2">Upper Age (months, 0-60):</label>
<input type="number" id="ageInputBaselineUpper" min="0" max="60" value="10"
class="shadow-sm appearance-none border rounded-lg w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:ring-2 focus:ring-purple-500 focus:border-transparent transition duration-200 ease-in-out">
</div>
<button id="addBaselineDataBtn"
class="flex-shrink-0 text-white font-bold py-2.5 px-4 rounded-lg focus:outline-none focus:ring-2 focus:ring-purple-500 focus:ring-offset-2 transition duration-300 ease-in-out transform hover:scale-105 shadow-md">
Add Baseline Age Range
</button>
</div>
<h3 class="text-xl font-semibold text-gray-800 mt-8 mb-4 text-center lg:text-left">Current Baseline Age Ranges:</h3>
<div id="baselineDataPointsList" class="bg-white p-4 rounded-lg border border-gray-200 max-h-60 overflow-y-auto mb-8">
<p class="text-gray-500 text-center" id="noBaselineMessage">No baseline age ranges defined.</p>
<!-- Baseline data points will be listed here -->
</div>
<button id="clearBaselineDataBtn" class="clear-btn">
Clear All Baseline Data
</button>
<h3 class="text-xl font-semibold text-gray-800 mt-8 mb-4 text-center lg:text-left">Your Data Points:</h3>
<div id="userDataPointsList" class="bg-white p-4 rounded-lg border border-gray-200 max-h-60 overflow-y-auto">
<p class="text-gray-500 text-center" id="noUserDataMessage">No user data points added yet.</p>
<!-- User data points will be listed here -->
</div>
<button id="clearUserDataBtn" class="clear-btn">
Clear All Your Data
</button>
</div>
<!-- Graph Section -->
<div class="graph-section flex flex-col items-center justify-center">
<h2 class="text-2xl font-bold text-gray-800 mb-6 text-center">Social Emotional Milestones Graph</h2>
<canvas id="milestoneGraph" width="800" height="600"></canvas>
</div>
</div>
</div>
<script>
// Get references to HTML elements for user data
const ageInput = document.getElementById('ageInput');
const milestoneInput = document.getElementById('milestoneInput');
const addDataBtn = document.getElementById('addDataBtn');
const userDataPointsList = document.getElementById('userDataPointsList');
const noUserDataMessage = document.getElementById('noUserDataMessage');
const clearUserDataBtn = document.getElementById('clearUserDataBtn');
// Get references to HTML elements for baseline data
const milestoneInputBaseline = document.getElementById('milestoneInputBaseline');
const ageInputBaselineLower = document.getElementById('ageInputBaselineLower');
const ageInputBaselineUpper = document.getElementById('ageInputBaselineUpper');
const addBaselineDataBtn = document.getElementById('addBaselineDataBtn');
const baselineDataPointsList = document.getElementById('baselineDataPointsList');
const noBaselineMessage = document.getElementById('noBaselineMessage');
const clearBaselineDataBtn = document.getElementById('clearBaselineDataBtn');
const messageBox = document.getElementById('messageBox');
const canvas = document.getElementById('milestoneGraph');
const ctx = canvas.getContext('2d');
// Define milestone labels, including "No Milestone" for index 0
const milestoneLabels = [
"No Milestone", // Index 0
"Milestone 1",
"Milestone 2",
"Milestone 3",
"Milestone 4",
"Milestone 5",
"Milestone 6"
];
// Editable set of baseline data points, now storing milestone, lowerAge, upperAge
let baselineDataPoints = [
// Updated default ranges as per user's request
{ milestone: 0, lowerAge: 0, upperAge: 0, type: 'baseline' },
{ milestone: 1, lowerAge: 0, upperAge: 3, type: 'baseline' },
{ milestone: 2, lowerAge: 2, upperAge: 7, type: 'baseline' },
{ milestone: 3, lowerAge: 3, upperAge: 10, type: 'baseline' },
{ milestone: 4, lowerAge: 9, upperAge: 18, type: 'baseline' },
{ milestone: 5, lowerAge: 18, upperAge: 36, type: 'baseline' },
{ milestone: 6, lowerAge: 30, upperAge: 48, type: 'baseline' }
];
// Store user-added data points
let userDataPoints = []; // Array of { age: number, milestone: number, type: 'user' }
// Graph dimensions and padding
const padding = 60;
const graphWidth = canvas.width - 2 * padding;
const graphHeight = canvas.height - 2 * padding;
// X-axis: Age in Months (0-60)
const xAxisMax = 60;
const xAxisInterval = 6; // Mark every 6 months
const xAxisLabel = "Age in Months";
// Y-axis: Social Emotional Milestones (0-6)
const yAxisMin = 0;
const yAxisMax = 6;
const yAxisInterval = 1; // Mark every milestone
const yAxisLabel = "Social Emotional Milestones";
// Cross-hatch pattern for shading
let crossHatchPattern = null;
/**
* Creates a cross-hatch pattern for canvas filling.
* @returns {CanvasPattern} The created pattern.
*/
function createCrossHatchPattern() {
const patternCanvas = document.createElement('canvas');
patternCanvas.width = 10;
patternCanvas.height = 10;
const pctx = patternCanvas.getContext('2d');
pctx.strokeStyle = 'rgba(66, 135, 245, 0.4)'; /* Blue with transparency */
pctx.lineWidth = 1;
pctx.beginPath();
pctx.moveTo(0, 10);
pctx.lineTo(10, 0);
pctx.stroke();
pctx.beginPath();
pctx.moveTo(0, 0);
pctx.lineTo(10, 10);
pctx.stroke();
return ctx.createPattern(patternCanvas, 'repeat');
}
/**
* Displays a message in the message box.
* @param {string} message - The message to display.
* @param {string} type - The type of message ('success', 'error', 'info').
*/
function showMessage(message, type = 'error') {
messageBox.textContent = message;
messageBox.className = `mt-4 p-3 text-sm rounded-lg`;
if (type === 'error') {
messageBox.classList.add('text-red-700', 'bg-red-100', 'border', 'border-red-200');
} else if (type === 'success') {
messageBox.classList.add('text-green-700', 'bg-green-100', 'border', 'border-green-200');
} else { // info
messageBox.classList.add('text-blue-700', 'bg-blue-100', 'border', 'border-blue-200');
}
messageBox.classList.remove('hidden');
// Hide message after 5 seconds
setTimeout(() => {
messageBox.classList.add('hidden');
}, 5000);
}
/**
* Converts an age value to its corresponding X-coordinate on the canvas.
* @param {number} age - The age in months.
* @returns {number} The X-coordinate.
*/
function getXCoordinate(age) {
return padding + (age / xAxisMax) * graphWidth;
}
/**
* Converts a milestone value to its corresponding Y-coordinate on the canvas.
* Note: Y-axis is inverted (0 at top, max at bottom).
* @param {number} milestone - The milestone value (0-6).
* @returns {number} The Y-coordinate.
*/
function getYCoordinate(milestone) {
// Clamp milestone value to the valid range [yAxisMin, yAxisMax] for plotting
const clampedMilestone = Math.max(yAxisMin, Math.min(yAxisMax, milestone));
return padding + graphHeight - ((clampedMilestone - yAxisMin) / (yAxisMax - yAxisMin)) * graphHeight;
}
/**
* Draws the X and Y axes, labels, and tick marks.
*/
function drawAxes() {
ctx.clearRect(0, 0, canvas.width, canvas.height); // Clear canvas before drawing
ctx.beginPath();
ctx.strokeStyle = '#333'; // Darker color for axes
ctx.lineWidth = 2;
// X-axis
ctx.moveTo(padding, padding + graphHeight);
ctx.lineTo(padding + graphWidth, padding + graphHeight);
// Y-axis
ctx.moveTo(padding, padding);
ctx.lineTo(padding, padding + graphHeight);
ctx.stroke();
ctx.fillStyle = '#333';
ctx.font = '14px Inter';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
// X-axis labels and ticks
for (let i = 0; i <= xAxisMax; i += xAxisInterval) {
const x = getXCoordinate(i);
ctx.beginPath();
ctx.moveTo(x, padding + graphHeight);
ctx.lineTo(x, padding + graphHeight + 5); // Tick mark
ctx.stroke();
ctx.fillText(i.toString(), x, padding + graphHeight + 20);
}
ctx.fillText(xAxisLabel, padding + graphWidth / 2, padding + graphHeight + 45);
// Y-axis labels and ticks
ctx.textAlign = 'right';
for (let i = yAxisMin; i <= yAxisMax; i += yAxisInterval) {
const y = getYCoordinate(i);
ctx.beginPath();
ctx.moveTo(padding, y);
ctx.lineTo(padding - 5, y); // Tick mark
ctx.stroke();
// Display milestone text label
ctx.fillText(milestoneLabels[i], padding - 10, y);
}
ctx.save(); // Save context state
ctx.translate(padding - 45, padding + graphHeight / 2); // Translate to position for vertical text
ctx.rotate(-Math.PI / 2); // Rotate 90 degrees counter-clockwise
ctx.fillText(yAxisLabel, 0, 0); // Draw text
ctx.restore(); // Restore context state
}
/**
* Plots a single line dataset, ensuring it starts from (0,0) if no point at age 0 exists.
* @param {Array<Object>} points - An array of data points to plot (e.g., {age, milestone}).
* @param {string} lineColor - The color of the line.
* @param {string} pointColor - The color of the points.
*/
function plotSingleLine(points, lineColor, pointColor) {
let pointsToPlot = [...points]; // Copy original points
// Add (0,0) if not explicitly present to ensure line starts from origin
const originExists = pointsToPlot.some(p => p.age === 0 && p.milestone === 0);
if (!originExists) {
pointsToPlot.push({ age: 0, milestone: 0 });
}
// Sort points by age for correct line plotting
pointsToPlot.sort((a, b) => a.age - b.age);
ctx.beginPath();
ctx.strokeStyle = lineColor;
ctx.lineWidth = 3;
// Move to the first point
ctx.moveTo(getXCoordinate(pointsToPlot[0].age), getYCoordinate(pointsToPlot[0].milestone));
// Draw lines to subsequent points
for (let i = 1; i < pointsToPlot.length; i++) {
const point = pointsToPlot[i];
ctx.lineTo(getXCoordinate(point.age), getYCoordinate(point.milestone));
}
ctx.stroke();
// Draw points (only for the original data points, not the synthetic (0,0) if it was added)
ctx.fillStyle = pointColor;
points.forEach(point => {
const x = getXCoordinate(point.age);
const y = getYCoordinate(point.milestone);
ctx.beginPath();
ctx.arc(x, y, 5, 0, Math.PI * 2); // Draw a circle for each point
ctx.fill();
ctx.closePath();
});
// If (0,0) was not an original point, draw a point for it.
if (!originExists) {
const x = getXCoordinate(0);
const y = getYCoordinate(0);
ctx.beginPath();
ctx.arc(x, y, 5, 0, Math.PI * 2);
ctx.fill();
ctx.closePath();
}
}
/**
* Plots the baseline range as a shaded area with two bounding lines.
* @param {Array<Object>} baselinePoints - Array of {milestone, lowerAge, upperAge}.
* @param {CanvasPattern} pattern - The pattern for shading.
*/
function plotBaselineRange(baselinePoints, pattern) {
if (baselinePoints.length === 0) return;
// Sort baseline points by milestone for correct plotting
baselinePoints.sort((a, b) => a.milestone - b.milestone);
// Prepare points for lower and upper age lines, including (0,0) if not present
const lowerAgeLinePoints = [];
const upperAgeLinePoints = [];
// Add (0,0) to ensure lines start from origin if no milestone 0 point is provided
const originMilestone0 = baselinePoints.find(p => p.milestone === 0);
if (!originMilestone0) {
lowerAgeLinePoints.push({ age: 0, milestone: 0 });
upperAgeLinePoints.push({ age: 0, milestone: 0 });
}
baselinePoints.forEach(p => {
lowerAgeLinePoints.push({ age: p.lowerAge, milestone: p.milestone });
upperAgeLinePoints.push({ age: p.upperAge, milestone: p.milestone });
});
// Sort these temporary arrays by age
lowerAgeLinePoints.sort((a, b) => a.age - b.age);
upperAgeLinePoints.sort((a, b) => a.age - b.age);
ctx.beginPath();
// Draw along the lower boundary
ctx.moveTo(getXCoordinate(lowerAgeLinePoints[0].age), getYCoordinate(lowerAgeLinePoints[0].milestone));
for (let i = 1; i < lowerAgeLinePoints.length; i++) {
const point = lowerAgeLinePoints[i];
ctx.lineTo(getXCoordinate(point.age), getYCoordinate(point.milestone));
}
// Draw back along the upper boundary in reverse order
for (let i = upperAgeLinePoints.length - 1; i >= 0; i--) {
const point = upperAgeLinePoints[i];
ctx.lineTo(getXCoordinate(point.age), getYCoordinate(point.milestone));
}
ctx.closePath();
ctx.fillStyle = pattern;
ctx.fill();
// Draw the lower age boundary line
ctx.beginPath();
ctx.strokeStyle = '#6b7280'; // Darker gray
ctx.lineWidth = 2;
ctx.moveTo(getXCoordinate(lowerAgeLinePoints[0].age), getYCoordinate(lowerAgeLinePoints[0].milestone));
for (let i = 1; i < lowerAgeLinePoints.length; i++) {
const point = lowerAgeLinePoints[i];
ctx.lineTo(getXCoordinate(point.age), getYCoordinate(point.milestone));
}
ctx.stroke();
// Draw the upper age boundary line
ctx.beginPath();
ctx.strokeStyle = '#9ca3af'; // Lighter gray
ctx.lineWidth = 2;
ctx.moveTo(getXCoordinate(upperAgeLinePoints[0].age), getYCoordinate(upperAgeLinePoints[0].milestone));
for (let i = 1; i < upperAgeLinePoints.length; i++) {
const point = upperAgeLinePoints[i];
ctx.lineTo(getXCoordinate(point.age), getYCoordinate(point.milestone));
}
ctx.stroke();
// Draw points for lower and upper bounds
ctx.fillStyle = '#4b5563'; // Darker gray for points
baselinePoints.forEach(point => {
ctx.beginPath();
ctx.arc(getXCoordinate(point.lowerAge), getYCoordinate(point.milestone), 5, 0, Math.PI * 2);
ctx.fill();
ctx.closePath();
ctx.beginPath();
ctx.arc(getXCoordinate(point.upperAge), getYCoordinate(point.milestone), 5, 0, Math.PI * 2);
ctx.fill();
ctx.closePath();
});
}
/**
* Redraws the entire graph.
*/
function redrawGraph() {
drawAxes();
plotBaselineRange(baselineDataPoints, crossHatchPattern); // Plot baseline range first
plotSingleLine(userDataPoints, '#3b82f6', '#1e40af'); // Then plot user data on top
}
/**
* Updates the list of baseline data points displayed on the page.
*/
function updateBaselineDataPointsList() {
baselineDataPointsList.innerHTML = ''; // Clear existing list
if (baselineDataPoints.length === 0) {
noBaselineMessage.classList.remove('hidden');
baselineDataPointsList.appendChild(noBaselineMessage);
return;
} else {
noBaselineMessage.classList.add('hidden');
}
baselineDataPoints.forEach((point, index) => {
const listItem = document.createElement('div');
listItem.className = 'baseline-point-item text-gray-700 text-base';
let milestoneLabelText = milestoneLabels[point.milestone];
if (point.milestone < yAxisMin || point.milestone > yAxisMax) {
milestoneLabelText += ' (Out of Range)';
}
if (point.lowerAge < 0 || point.lowerAge > xAxisMax || point.upperAge < 0 || point.upperAge > xAxisMax) {
milestoneLabelText += ' (Age Out of Range)';
}
listItem.innerHTML = `
<span>Milestone: ${point.milestone} (${milestoneLabelText}), Age Range: ${point.lowerAge} - ${point.upperAge} months</span>
<button class="delete-btn" data-index="${index}" data-type="baseline">Delete</button>
`;
baselineDataPointsList.appendChild(listItem);
});
// Add event listeners to delete buttons for baseline points
document.querySelectorAll('#baselineDataPointsList .delete-btn').forEach(button => {
button.addEventListener('click', (event) => {
const indexToDelete = parseInt(event.target.dataset.index);
deleteDataPoint(indexToDelete, 'baseline');
});
});
}
/**
* Updates the list of user-added data points displayed on the page.
*/
function updateUserDataPointsList() {
userDataPointsList.innerHTML = ''; // Clear existing list
if (userDataPoints.length === 0) {
noUserDataMessage.classList.remove('hidden');
userDataPointsList.appendChild(noUserDataMessage);
return;
} else {
noUserDataMessage.classList.add('hidden');
}
userDataPoints.forEach((point, index) => {
const listItem = document.createElement('div');
listItem.className = 'data-point-item text-gray-700 text-base';
listItem.innerHTML = `
<span>Age: ${point.age} months, Milestone: ${milestoneLabels[point.milestone]}</span>
<button class="delete-btn" data-index="${index}" data-type="user">Delete</button>
`;
userDataPointsList.appendChild(listItem);
});
// Add event listeners to delete buttons for user points
document.querySelectorAll('#userDataPointsList .delete-btn').forEach(button => {
button.addEventListener('click', (event) => {
const indexToDelete = parseInt(event.target.dataset.index);
deleteDataPoint(indexToDelete, 'user');
});
});
}
/**
* Deletes a data point from the specified array.
* @param {number} index - The index of the data point to delete.
* @param {string} type - The type of data ('user' or 'baseline').
*/
function deleteDataPoint(index, type) {
if (type === 'user') {
if (index >= 0 && index < userDataPoints.length) {
userDataPoints.splice(index, 1);
updateUserDataPointsList();
showMessage('Your data point deleted.', 'info');
}
} else if (type === 'baseline') {
if (index >= 0 && index < baselineDataPoints.length) {
baselineDataPoints.splice(index, 1);
updateBaselineDataPointsList();
showMessage('Baseline data point deleted.', 'info');
}
}
redrawGraph();
}
// Event listener for adding user data points
addDataBtn.addEventListener('click', () => {
const age = parseInt(ageInput.value);
const milestone = parseInt(milestoneInput.value);
// Input validation
if (isNaN(age) || isNaN(milestone)) {
showMessage('Please enter valid numbers for Age and Milestone for your data.', 'error');
return;
}
if (age < 0 || age > xAxisMax) {
showMessage(`Age in Months for your data must be between 0 and ${xAxisMax}.`, 'error');
return;
}
if (milestone < 0 || milestone > yAxisMax) {
showMessage(`Social Emotional Milestone for your data must be between 0 and ${yAxisMax}.`, 'error');
return;
}
// Check for duplicate age in user data, if so, update the milestone
const existingPointIndex = userDataPoints.findIndex(point => point.age === age);
if (existingPointIndex !== -1) {
userDataPoints[existingPointIndex].milestone = milestone;
showMessage(`Updated milestone for Age ${age} in your data to ${milestoneLabels[milestone]}.`, 'success');
} else {
userDataPoints.push({ age, milestone, type: 'user' });
showMessage('Your data point added successfully!', 'success');
}
updateUserDataPointsList();
redrawGraph();
// Reset input fields
ageInput.value = 0;
milestoneInput.value = 0;
});
// Event listener for adding baseline data points
addBaselineDataBtn.addEventListener('click', () => {
const milestone = parseInt(milestoneInputBaseline.value);
const lowerAge = parseInt(ageInputBaselineLower.value);
const upperAge = parseInt(ageInputBaselineUpper.value);
// Input validation
if (isNaN(milestone) || isNaN(lowerAge) || isNaN(upperAge)) {
showMessage('Please enter valid numbers for Milestone, Lower Age, and Upper Age for baseline data.', 'error');
return;
}
if (milestone < 0 || milestone > yAxisMax) {
showMessage(`Milestone for baseline data must be between 0 and ${yAxisMax}.`, 'error');
return;
}
if (lowerAge < 0 || lowerAge > xAxisMax || upperAge < 0 || upperAge > xAxisMax) {
showMessage(`Lower and Upper Ages for baseline data must be between 0 and ${xAxisMax}.`, 'error');
return;
}
if (lowerAge > upperAge) {
showMessage('Lower Age cannot be greater than Upper Age for baseline data.', 'error');
return;
}
// Check for duplicate milestone in baseline data, if so, update the range
const existingPointIndex = baselineDataPoints.findIndex(point => point.milestone === milestone);
if (existingPointIndex !== -1) {
baselineDataPoints[existingPointIndex].lowerAge = lowerAge;
baselineDataPoints[existingPointIndex].upperAge = upperAge;
showMessage(`Updated baseline age range for Milestone ${milestone} to ${lowerAge}-${upperAge} months.`, 'success');
} else {
baselineDataPoints.push({ milestone, lowerAge, upperAge, type: 'baseline' });
showMessage('Baseline age range added successfully!', 'success');
}
updateBaselineDataPointsList();
redrawGraph();
// Reset input fields
milestoneInputBaseline.value = 0;
ageInputBaselineLower.value = 0;
ageInputBaselineUpper.value = 10;
});
// Event listener for clearing all user data
clearUserDataBtn.addEventListener('click', () => {
userDataPoints = []; // Clear the array
updateUserDataPointsList(); // Update the displayed list
redrawGraph(); // Redraw the graph
showMessage('All your data points cleared.', 'info');
});
// Event listener for clearing all baseline data
clearBaselineDataBtn.addEventListener('click', () => {
baselineDataPoints = []; // Clear the array
updateBaselineDataPointsList(); // Update the displayed list
redrawGraph(); // Redraw the graph
showMessage('All baseline data points cleared.', 'info');
});
// Initial drawing of the graph and list
window.onload = function() {
crossHatchPattern = createCrossHatchPattern(); // Initialize pattern once
redrawGraph();
updateBaselineDataPointsList();
updateUserDataPointsList();
};
// Handle canvas resizing for responsiveness (optional, but good practice)
window.addEventListener('resize', () => {
redrawGraph();
});
</script>