File size: 10,575 Bytes
f7c7e26
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
// Get canvas and context
const canvas = document.getElementById('regressionCanvas');
const ctx = canvas.getContext('2d');

// Data from your Python script (X, y)
// These are hardcoded here for visualization purposes.
// In a real advanced app, these might be dynamically loaded.
const X_data = [1, 2, 3, 4, 5];
const y_data = [35, 45, 55, 65, 75];

// --- Understanding Slope (m) and Intercept (b) ---
// For a perfect linear relationship as in your data,
// we can manually calculate slope (m) and intercept (b).
// In a real-world scenario with scattered data, the scikit-learn
// LinearRegression model uses more advanced statistical methods
// (like Ordinary Least Squares) to find the 'best fit' line
// that minimizes the squared differences between actual and predicted y values.

// Calculate Slope (m):
// m = (y2 - y1) / (x2 - x1)
// Using points (1, 35) and (2, 45):
// m = (45 - 35) / (2 - 1) = 10 / 1 = 10
const slope = 10;

// Calculate Intercept (b):
// b = y - m * x
// Using point (1, 35) and calculated slope m=10:
// b = 35 - (10 * 1) = 35 - 10 = 25
const intercept = 25;

// Display slope and intercept values in the HTML
document.getElementById('slopeValue').textContent = slope.toFixed(2);
document.getElementById('interceptValue').textContent = intercept.toFixed(2);

// Canvas dimensions and padding
let canvasWidth, canvasHeight;
const padding = 50;

// Scale factors for drawing data onto the canvas
let xScale, yScale;
let xMin, xMax, yMin, yMax;

// Prediction variables (these will be updated when the user inputs hours)
let predictedHours = null;
let predictedScore = null;

// Function to set up scaling based on data range and canvas size
function setupScaling() {
    canvasWidth = canvas.width;
    canvasHeight = canvas.height;

    // Determine data ranges for X and Y axes
    xMin = Math.min(...X_data, 0); // Always start X-axis at 0
    // Set xMax to at least 10 (as per the last request) and ensure it covers any new predicted hours
    xMax = Math.max(...X_data, predictedHours !== null ? predictedHours : 0, 10) + 1; // Extend x-axis slightly beyond 10
    
    yMin = Math.min(...y_data, 0); // Always start Y-axis at 0
    // Calculate the predicted score for the determined xMax to ensure the y-axis covers the line
    const maxPredictedY = slope * xMax + intercept;
    yMax = Math.max(...y_data, predictedScore !== null ? predictedScore : 0, maxPredictedY) + 20; // Extend y-axis slightly beyond max needed

    // Calculate scaling factors to fit data within the canvas padding
    xScale = (canvasWidth - 2 * padding) / (xMax - xMin);
    yScale = (canvasHeight - 2 * padding) / (yMax - yMin);
}

// Convert data coordinates (e.g., hours, score) to canvas pixel coordinates
function toCanvasX(x) {
    return padding + (x - xMin) * xScale;
}

function toCanvasY(y) {
    return canvasHeight - padding - (y - yMin) * yScale;
}

// Function to draw the entire graph, including data points, regression line, and predictions
function drawGraph() {
    ctx.clearRect(0, 0, canvasWidth, canvasHeight); // Clear the entire canvas

    // Draw axes
    ctx.beginPath();
    ctx.strokeStyle = '#64748b'; // Slate gray for axes
    ctx.lineWidth = 2;

    // X-axis (horizontal line)
    ctx.moveTo(padding, toCanvasY(yMin));
    ctx.lineTo(canvasWidth - padding, toCanvasY(yMin));
    // Y-axis (vertical line)
    ctx.moveTo(toCanvasX(xMin), padding);
    ctx.lineTo(toCanvasX(xMin), canvasHeight - padding);
    ctx.stroke();

    // Draw axis labels and ticks
    ctx.fillStyle = '#475569'; // Darker gray for labels
    ctx.font = '14px Inter';
    ctx.textAlign = 'center';
    ctx.textBaseline = 'top';

    // X-axis labels (Hours Studied)
    // Dynamic tick step for clarity on different scales
    const xTickStep = 1; // Every 1 hour for a graph up to 10
    for (let i = Math.ceil(xMin / xTickStep) * xTickStep; i <= Math.floor(xMax); i += xTickStep) {
        if (i >= 0) {
            ctx.fillText(i + 'h', toCanvasX(i), canvasHeight - padding + 10);
            ctx.beginPath();
            ctx.moveTo(toCanvasX(i), canvasHeight - padding);
            ctx.lineTo(toCanvasX(i), canvasHeight - padding - 5);
            ctx.stroke();
        }
    }
    // X-axis title
    ctx.fillText('Hours Studied', canvasWidth / 2, canvasHeight - 20);

    ctx.textAlign = 'right';
    ctx.textBaseline = 'middle';
    // Y-axis labels (Score)
    // Dynamic tick step for clarity on different scales
    const yTickStep = (yMax - yMin) / 10 > 20 ? 50 : 20; // Example: every 20 or 50 points
    for (let i = Math.ceil(yMin / yTickStep) * yTickStep; i <= Math.floor(yMax); i += yTickStep) {
        if (i >= 0) {
            ctx.fillText(i.toFixed(0), padding - 10, toCanvasY(i));
            ctx.beginPath();
            ctx.moveTo(padding, toCanvasY(i));
            ctx.lineTo(padding + 5, toCanvasY(i));
            ctx.stroke();
        }
    }
    // Y-axis title (rotated)
    ctx.save();
    ctx.translate(20, canvasHeight / 2);
    ctx.rotate(-Math.PI / 2);
    ctx.textAlign = 'center';
    ctx.fillText('Score', 0, 0);
    ctx.restore();


    // Draw data points (blue circles)
    ctx.fillStyle = '#3b82f6'; // Blue for data points
    X_data.forEach((x, i) => {
        ctx.beginPath();
        ctx.arc(toCanvasX(x), toCanvasY(y_data[i]), 5, 0, Math.PI * 2); // Radius 5
        ctx.fill();
    });

    // Draw regression line (red line)
    ctx.beginPath();
    ctx.strokeStyle = '#ef4444'; // Red for regression line
    ctx.lineWidth = 3;
    // Draw line across the entire X-axis range based on the model equation
    ctx.moveTo(toCanvasX(xMin), toCanvasY(slope * xMin + intercept));
    ctx.lineTo(toCanvasX(xMax), toCanvasY(slope * xMax + intercept));
    ctx.stroke();

    // Draw predicted point and lines if available (green point and dashed lines)
    if (predictedHours !== null && predictedScore !== null) {
        const predX = toCanvasX(predictedHours);
        const predY = toCanvasY(predictedScore);

        // Predicted point
        ctx.fillStyle = '#22c55e'; // Green for predicted point
        ctx.beginPath();
        ctx.arc(predX, predY, 6, 0, Math.PI * 2); // Slightly larger radius
        ctx.fill();

        // Dotted lines to axes
        ctx.strokeStyle = '#22c55e'; // Green for dotted lines
        ctx.lineWidth = 1.5;
        ctx.setLineDash([5, 5]); // Dotted line style

        // Line from predicted point to X-axis
        ctx.beginPath();
        ctx.moveTo(predX, predY);
        ctx.lineTo(predX, toCanvasY(yMin));
        ctx.stroke();

        // Line from predicted point to Y-axis
        ctx.beginPath();
        ctx.moveTo(predX, predY);
        ctx.lineTo(toCanvasX(xMin), predY);
        ctx.stroke();

        ctx.setLineDash([]); // Reset line dash to solid for subsequent drawings
    }
}

// Event listener for the "Predict Score" button click
document.getElementById('predictBtn').addEventListener('click', () => {
    // Get the value from the input field and parse it as a floating-point number
    const hoursInput = parseFloat(document.getElementById('hoursInput').value);

    // Check if the input is a valid number
    if (!isNaN(hoursInput)) {
        // Update global prediction variables
        predictedHours = hoursInput;
        predictedScore = slope * predictedHours + intercept;

        // Display the predicted score in the HTML
        document.getElementById('predictedScore').textContent = predictedScore.toFixed(2);
        // Make the prediction output box visible
        document.getElementById('predictionOutput').classList.remove('hidden');

        // Recalculate scaling and redraw the graph to accommodate new prediction if it extends axes
        setupScaling(); 
        drawGraph();
    } else {
        // If input is invalid, display an error message
        const outputDiv = document.getElementById('predictionOutput');
        outputDiv.innerHTML = '<p class="text-red-600">Please enter a valid number for hours studied.</p>';
        outputDiv.classList.remove('hidden');
    }
});

// Function to handle canvas resizing and redraw the graph
function resizeCanvas() {
    // Get the device pixel ratio for sharper rendering on high-DPI screens
    const dpi = window.devicePixelRatio; 
    // Get the actual rendered size of the canvas element from its CSS styles
    const rect = canvas.getBoundingClientRect(); 

    // Set the internal drawing buffer size of the canvas
    canvas.width = rect.width * dpi;
    canvas.height = rect.height * dpi;

    // Scale the drawing context to match the DPI, ensuring crisp lines and text
    ctx.scale(dpi, dpi); 

    // Re-setup scaling for data to canvas coordinates and redraw
    setupScaling();
    drawGraph();
}

// Initial setup and draw when the window loads
window.addEventListener('load', () => {
    resizeCanvas(); // Set initial canvas size and draw
    // Also trigger an initial prediction for the default value in the input field
    const initialHours = parseFloat(document.getElementById('hoursInput').value);
    if (!isNaN(initialHours)) {
        predictedHours = initialHours;
        predictedScore = slope * initialHours + intercept; 
        document.getElementById('predictedScore').textContent = predictedScore.toFixed(2);
        document.getElementById('predictionOutput').classList.remove('hidden');
        setupScaling();
        drawGraph();
    }
});

// Redraw the graph whenever the window is resized
window.addEventListener('resize', resizeCanvas); 

// Optional: Allow clicking on canvas to set hours input (for quick testing)
canvas.addEventListener('click', (event) => {
    // Get mouse click coordinates relative to the canvas
    const rect = canvas.getBoundingClientRect();
    const mouseX = (event.clientX - rect.left) / (canvas.width / canvas.getBoundingClientRect().width); 
    const mouseY = (event.clientY - rect.top) / (canvas.height / canvas.getBoundingClientRect().height); // Corrected this line

    // Convert canvas X coordinate back to data X (hours studied)
    const clickedHours = xMin + (mouseX - padding) / xScale;
    // Update the input field with the clicked hours
    document.getElementById('hoursInput').value = clickedHours.toFixed(1);
    // Trigger the prediction immediately
    document.getElementById('predictBtn').click();
});