Untitled

mail@pastecode.io avatar
unknown
javascript
2 years ago
7.8 kB
31
Indexable
Never
import {ticks, format_value} from './util.js';
import {parse} from './evaluator/parse.js';
import {build_expression} from './evaluator/expression.js';

const container = document.querySelector(".container");
const error_box = document.querySelector(".error");
const canvas = document.querySelector("canvas");
const min_max_button = document.querySelector(".min_max_button");
const draw_button = document.querySelector("#evaluate");
const expression_input = document.querySelector("#expression");
const min_domain_input = document.querySelector("#x_min");
const max_domain_input = document.querySelector("#x_max");
const form = document.querySelector('form');

const ORIGIN_OFFSET = 60;

form.addEventListener('submit', function(event) {
    event.preventDefault();
    clear();
    if(compute()) draw();
});

draw_button.addEventListener('click', function() {
    clear();
    if(compute()) draw();
})

min_max_button.addEventListener('click', function() {
    if(min_max_button.getAttribute('class') === 'min_max_button minimise') {
        min_max_button.setAttribute('class', 'min_max_button');
        container.setAttribute('class', 'container');
    } else {
        min_max_button.setAttribute('class', 'min_max_button minimise');
        container.setAttribute('class', 'container maximised');
    }
    window_resize();
    clear();
    draw();
})

window.addEventListener('resize',function() {
    window_resize();
    clear();
    draw();
})

window.addEventListener('load', (event) => {
    let url_args = window.location.search.slice(1);
    let params = url_args.split('&')
    if(params.length === 3 && params[1].startsWith('start=') && params[2].startsWith('end=')) {
        expression_input.value = params[0];
        min_domain_input.value = params[1].slice(6);
        max_domain_input.value = params[2].slice(4);
    } else {
        expression_input.value = 'x';
        min_domain_input.value = '-1.0';
        max_domain_input.value = '1.0';
    }
    compute();
    draw();
})

const X = new Array(100);
const Y = new Array(100);
for(let i = 0; i < X.length; i++) {
    X[i] = 0.01*i;
}

class ToCanvas {
    constructor(dmin, dmax, rmin, rmax) {
        this.dmin = dmin;
        this.dmax = dmax;
        this.rmin = rmin;
        this.rmax = rmax;
    }

    convert(value) {
        return this.rmin + (this.rmax - this.rmin) * (value - this.dmin) / (this.dmax - this.dmin);
    }
}

function range(arr) {
    let max = Number.MIN_VALUE;
    let min = Number.MAX_VALUE;
    for(let item of arr) {
        max = (item > max) ? item : max;
        min = (item < min) ? item : min;
    }
    return [min, max];
}

function window_resize() {
    let ratio = window.devicePixelRatio;
    canvas.width = canvas.offsetWidth * ratio;
    canvas.height = canvas.offsetHeight * ratio;
}

let x_ticks = []
let y_ticks = []

function _compute_with_possible_errors() {
    let X_min = Number(min_domain_input.value);
    let X_max = Number(max_domain_input.value);
    let X_range = X_max - X_min;

    if(X_range <= 0) {
        throw new Error("Range of domain must be > 0");
    }

    let tokens = parse(expression_input.value);
    console.log(tokens);
    let exp = build_expression(tokens);
    console.log(exp.toString());

    let evaluation_error = ''
    for(let i = 0; i < X.length; i++) {
        try {
            X[i] = X_min + (X_range)*(i/100);
            Y[i] = exp.evaluate({x: X[i]});
        } catch (e) {
            evaluation_error = e.name + ': ' + e.message;
            console.error(evaluation_error);
            Y[i] = Number.NaN;
        }
    }

    if(Y.every(x => Number.isNaN(x))) {
        throw new Error(evaluation_error);
    }

    let [Y_min, Y_max] = range(Y);
    y_ticks = ticks(Y_min, Y_max);
    x_ticks = ticks(X_min, X_max);
    console.log(x_ticks);
    console.log(y_ticks);
}

function compute() {
    try {
        _compute_with_possible_errors();
        error_box.textContent = '';
        return true;
    } catch(e) {
        if(e.message.toString().startsWith('Error:')) {
            e.message = e.message.slice(6);
        }
        x_ticks = []
        y_ticks = []
        error_box.textContent = e.name + ': ' + e.message;
        console.log(e.name + ': ' + e.message);
    }
    return false;
}

function clear() {
    let ctx = canvas.getContext("2d");
    ctx.clearRect(0, 0, canvas.width, canvas.height);
}

function draw() {
    let ctx = canvas.getContext("2d");
    ctx.lineWidth = 1;
    ctx.strokeStyle = 'rgb(0,0,0)';
    ctx.font = '15px sans-serif';

    let [X_min, X_max] = range(X);
    let [Y_min, Y_max] = range(Y);

    let cx = new ToCanvas(X_min, X_max, ORIGIN_OFFSET, canvas.width);
    let cy = new ToCanvas(Y_min, Y_max, ORIGIN_OFFSET, canvas.height);

    // Axes
    ctx.beginPath();
    ctx.moveTo(cx.convert(X_min), canvas.height - cy.convert(Y_min));
    ctx.lineTo(cx.convert(X_max), canvas.height - cy.convert(Y_min));
    ctx.moveTo(cx.convert(X_min), canvas.height - cy.convert(Y_min));
    ctx.lineTo(cx.convert(X_min), canvas.height - cy.convert(Y_max));

    // Axes Labels
    let x_axes_y = canvas.height - cy.convert(Y_min);
    for(let tick of x_ticks) {
        // TODO: Don't have this tick to start with!
        if(tick < X_min) {
            continue;
        }
        let label = format_value(tick);
        let label_width = ctx.measureText(label).width;
        ctx.fillText(label, cx.convert(tick) - label_width/2, x_axes_y + 20);
        ctx.moveTo(cx.convert(tick), x_axes_y + 5);
        ctx.lineTo(cx.convert(tick), x_axes_y);
    }

    let y_axes_x = cx.convert(X_min);
    // TODO: Use label height and width
    for(let tick of y_ticks) {
        // TODO: Don't have this tick to start with!
        if(tick < Y_min) {
            continue;
        }
        let label = format_value(tick);
        let label_width = ctx.measureText(label).width;
        ctx.fillText(label, y_axes_x - label_width - 8, canvas.height - cy.convert(tick) + 5);
        ctx.moveTo(y_axes_x - 5, canvas.height - cy.convert(tick));
        ctx.lineTo(y_axes_x, canvas.height - cy.convert(tick));
    }
    ctx.stroke();

    // Grid lines
    ctx.strokeStyle = 'rgb(189,189,189)';
    ctx.setLineDash([5, 3]);
    ctx.beginPath();
    for(let tick of y_ticks.slice(1).filter(x => x !== 0)) {
        ctx.moveTo(y_axes_x, canvas.height - cy.convert(tick));
        ctx.lineTo(canvas.width, canvas.height - cy.convert(tick));
    }
    for(let tick of x_ticks.slice(1).filter(x => x !== 0)) {
        ctx.moveTo(cx.convert(tick), x_axes_y);
        ctx.lineTo(cx.convert(tick), 0);
    }
    ctx.stroke();

    ctx.setLineDash([]);
    ctx.strokeStyle = 'rgb(190,190,190)';

    ctx.beginPath();
    if (x_ticks.includes(0)) {
        ctx.moveTo(cx.convert(0), x_axes_y);
        ctx.lineTo(cx.convert(0), 0);
    }
    if (y_ticks.includes(0)) {
        ctx.moveTo(y_axes_x, canvas.height - cy.convert(0));
        ctx.lineTo(canvas.width, canvas.height - cy.convert(0));
    }
    ctx.stroke();

    ctx.strokeStyle = 'rgb(253,30,30)';
    ctx.lineWidth = 2;
    ctx.setLineDash([]);

    // Curve
    ctx.beginPath();
    let prevNaN = true;
    for(let i = 0; i < X.length - 1; i++) {
        let isNaN = Number.isNaN(X[i]) || Number.isNaN(Y[i]);
        if(!isNaN && prevNaN) {
            ctx.moveTo(cx.convert(X[i]), canvas.height - cy.convert(Y[i]));
        } else if(!isNaN && !prevNaN) {
            ctx.lineTo(cx.convert(X[i]), canvas.height - cy.convert(Y[i]));
        }
        prevNaN = isNaN;
    }
    ctx.stroke();
}

window_resize();