// ==UserScript==
// @name Dynamic Graph (Calc Extension)
// @version 1.7
// @description A script to plot graphs with improved plotting and axis markings
// @match *://*/*
// @namespace https://greasyfork.org/en/users/1291009
// @author BadOrBest
// @license MIT
// @icon https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcRbUKCIRZQujx1f9j9HfzO0igzHyCAFjAxYKQ&s
// @grant GM_addStyle
// @grant GM_setValue
// @grant GM_getValue
// @grant GM_deleteValue
// @grant GM_registerMenuCommand
// @grant GM.registerMenuCommand
// ==/UserScript==
(function() {
'use strict';
const defaultViewport = {
x: 0,
y: 0,
width: 600,
height: 400,
scale: 1
};
let viewport = { ...defaultViewport };
let isDragging = false;
let dragStart = { x: 0, y: 0 };
let renderingInterval;
// Create canvas and zoom control elements
const canvas = document.createElement('canvas');
canvas.id = 'graph-canvas';
canvas.width = 600;
canvas.height = 400;
canvas.style.border = '24px solid black';
canvas.style.borderRadius = '12px';
canvas.style.backgroundColor = 'white';
canvas.style.display = 'none';
canvas.style.position = 'absolute';
canvas.style.left = '100px';
canvas.style.top = '100px';
document.body.appendChild(canvas);
const zoomBar = document.createElement('div');
zoomBar.id = 'zoom-bar';
zoomBar.style.position = 'absolute';
zoomBar.style.top = '10px';
zoomBar.style.left = '10px';
zoomBar.style.padding = '5px';
zoomBar.style.backgroundColor = 'white';
zoomBar.style.border = '1px solid black';
zoomBar.style.borderRadius = '8px';
zoomBar.style.zIndex = '1001';
zoomBar.textContent = `Zoom: ${viewport.scale.toFixed(2)}x`;
document.body.appendChild(zoomBar);
function plotGraph(data) {
const context = canvas.getContext('2d');
const width = canvas.width;
const height = canvas.height;
context.clearRect(0, 0, width, height);
// Draw x and y axes only (no grid)
context.strokeStyle = 'black';
context.lineWidth = 2;
context.beginPath();
context.moveTo(width / 2, 0); // Y axis
context.lineTo(width / 2, height);
context.moveTo(0, height / 2); // X axis
context.lineTo(width, height / 2);
context.stroke();
// Draw graph line and plot points only when in view
context.strokeStyle = 'blue';
context.lineWidth = 2;
context.beginPath();
let first = true;
data.forEach(point => {
const screenX = (point.x - viewport.x) * viewport.scale + width / 2;
const screenY = height / 2 - (point.y - viewport.y) * viewport.scale;
// Plot the line and points only when they're in view
if (screenX >= 0 && screenX <= width && screenY >= 0 && screenY <= height) {
if (first) {
context.moveTo(screenX, screenY);
first = false;
} else {
context.lineTo(screenX, screenY);
}
// Plot the point
context.fillStyle = 'red';
context.beginPath();
context.arc(screenX, screenY, 3, 0, Math.PI * 2);
context.fill();
}
});
context.stroke();
}
function parseExpression(expression) {
const dataPoints = [];
const step = 1 / viewport.scale; // Increased granularity for better detail
let lastX = viewport.x - viewport.width / 2;
while (lastX <= viewport.x + viewport.width / 2) {
try {
const y = evaluateExpression(expression, lastX);
if (Math.abs(lastX) <= viewport.width / 2 && Math.abs(y) <= viewport.height / 2) {
dataPoints.push({ x: lastX, y });
}
} catch (e) {
console.error('Error evaluating expression:', e);
}
lastX += step;
}
return filterPlotPoints(dataPoints);
}
function filterPlotPoints(points) {
const filteredPoints = [];
let lastPoint = null;
points.forEach(point => {
if (!lastPoint || Math.abs(point.y - lastPoint.y) >= 2) { // Only add points with a gap of 2
filteredPoints.push(point);
lastPoint = point;
}
});
return filteredPoints;
}
function evaluateExpression(expr, x) {
return Function('x', `return ${expr.replace('^', '**')}`)(x);
}
function addGraphData() {
const expression = prompt('Enter a mathematical expression (e.g., x^2 + 2*x + 1):');
if (expression) {
const data = parseExpression(expression);
plotGraph(data);
saveGraphData(expression, data);
}
}
function clearGraphData() {
localStorage.removeItem('graphExpression');
localStorage.removeItem('graphData');
plotGraph([]); // Clear the graph
}
function saveGraphData(expression, data) {
try {
localStorage.setItem('graphExpression', expression);
localStorage.setItem('graphData', JSON.stringify(data));
} catch (error) {
console.error('Error saving graph data to localStorage:', error);
}
}
function loadGraphData() {
try {
const expression = localStorage.getItem('graphExpression');
const data = JSON.parse(localStorage.getItem('graphData'));
if (expression && data) {
plotGraph(data);
}
} catch (error) {
console.error('Error loading graph data from localStorage:', error);
}
}
function toggleGraph() {
const canvas = document.getElementById('graph-canvas');
if (canvas.style.display === 'none') {
canvas.style.display = 'block';
updateZoomBar();
loadGraphData(); // Load saved graph data
} else {
canvas.style.display = 'none';
}
}
function handleWheel(event) {
event.preventDefault();
const zoomFactor = 0.1;
const zoomIn = event.deltaY < 0;
const newScale = zoomIn ? viewport.scale * (1 + zoomFactor) : viewport.scale / (1 + zoomFactor);
viewport.scale = Math.max(Math.min(newScale, 20), 0.1); // Clamp scale between 0.1 and 20
const rect = canvas.getBoundingClientRect();
const mouseX = (event.clientX - rect.left - canvas.width / 2) / viewport.scale + viewport.x;
const mouseY = (event.clientY - rect.top - canvas.height / 2) / viewport.scale + viewport.y;
viewport.x = mouseX - (event.clientX - rect.left - canvas.width / 2) / viewport.scale;
viewport.y = mouseY - (event.clientY - rect.top - canvas.height / 2) / viewport.scale;
updateZoomBar();
const expression = localStorage.getItem('graphExpression');
if (expression) {
const data = parseExpression(expression);
plotGraph(data);
startRenderingInterval(true); // Faster rendering while zooming
}
}
function handleMouseDown(event) {
if (event.button === 0) { // Left mouse button
isDragging = true;
dragStart.x = event.clientX;
dragStart.y = event.clientY;
document.addEventListener('mousemove', handleMouseMove);
document.addEventListener('mouseup', handleMouseUp);
}
}
// Updated to sync canvas dragging with viewport
function handleMouseMove(event) {
if (!isDragging) return;
const dx = event.clientX - dragStart.x;
const dy = event.clientY - dragStart.y;
viewport.x -= dx / viewport.scale;
viewport.y += dy / viewport.scale;
dragStart.x = event.clientX;
dragStart.y = event.clientY;
plotGraph(parseExpression(localStorage.getItem('graphExpression')));
}
function handleMouseUp() {
isDragging = false;
document.removeEventListener('mousemove', handleMouseMove);
document.removeEventListener('mouseup', handleMouseUp);
}
function updateZoomBar() {
zoomBar.textContent = `Zoom: ${viewport.scale.toFixed(2)}x`;
}
function startRenderingInterval(zooming = false) {
stopRenderingInterval();
renderingInterval = setInterval(() => {
const expression = localStorage.getItem('graphExpression');
if (expression) {
const data = parseExpression(expression);
plotGraph(data);
}
}, zooming ? 500 : 1000); // Adjusted faster rendering when zooming
}
function stopRenderingInterval() {
if (renderingInterval) {
clearInterval(renderingInterval);
renderingInterval = null;
}
}
canvas.addEventListener('wheel', handleWheel);
canvas.addEventListener('mousedown', handleMouseDown);
const button = document.createElement('button');
button.textContent = 'Graph';
button.style.position = 'fixed';
button.style.top = '10px';
button.style.right = '10px';
button.style.zIndex = '1000';
button.addEventListener('click', toggleGraph);
document.body.appendChild(button);
GM_registerMenuCommand('Add Equation', addGraphData);
GM_registerMenuCommand('Clear Graph', clearGraphData);
updateZoomBar();
loadGraphData(); // Load saved graph data on script load
})();