Dynamic Graph (Calc Extension)

A script to plot graphs with improved plotting and axis markings

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。

您需要先安装用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

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