Dynamic Graph (Calc Extension)

A script to plot graphs with improved plotting and axis markings

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Userscripts to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install a user script manager extension to install this script.

(У мене вже є менеджер скриптів, дайте мені встановити його!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==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
})();