Рисуем графики онлайн

    Началось все с того, что около трех или четырех лет назад я написал на Java собственный парсер и калькулятор математических выражений — jExpressions.

    И вот, относительно недавно, в свете осваивания технологии Java EE, возникла идея сделать на основе этого парсера онлайн рисовалку графиков.

    Парсер на тот момент обладал довольно экзотическим синтаксисом для вызова функций (напр. exp#3 вместо exp(3); beta#1:2 вместо beta(1,2)).
    Также время от времени вылетали баги.

    После нескольких часов допиливания и обезглючивания появилась на свет версия jExpressions 1.0.

    После этого можно было приступить к делу.


    Рисовалка графиков устроена так:

    1. Cервлет вычисляет значения функции для N-ного количества точек (250 для простых и 1000 для параметрических) и выдает результат в формате json.
    2. Веб-страница получает json от сервлета и рисует график с помощью плагина Flot.

    Код функции processRequest() сервлета:

    private void processRequest(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
    	response.setContentType("text/plain;charset=UTF-8");
    	PrintWriter out = response.getWriter();
                    
    	String result = "{ \"result\" : ";
        
    	String func = request.getParameter("func");
    	String var = request.getParameter("variable");
    	String begin = request.getParameter("begin");
    	String end = request.getParameter("end");
            
    	if (func==null||var==null||begin==null||end==null) {
    		result += "\"error\", \"err\" : \"param\"}";
    		out.println(result);
    		return;
    	}
    
    	// upd:
            
    	//Double x0 = Double.parseDouble(begin);
    	//Double x1 = Double.parseDouble(end);
    
    	Double x0;
    	Double x1;
            
    	try {
    		x0 = Double.parseDouble(begin);
    		x1 = Double.parseDouble(end);
    	} catch (Exception e) {
    		result += "\"error\", \"err\" : \"param\"}";
    		out.println(result);
    		return;
    	}
            
    	int num = 0;
            try {        
    		Parser parser = new Parser();
            	
    		num = parser.setAlias(var); // сообщаем парсеру имя переменной
            	
    		long time0 = System.nanoTime();
            	
    		Expr expr = parser.parseExpr(parser.prepareExpr(func)); // на основе выражения строится двоичное дерево
            	
    		String xArr = "[";
    		String yArr = "[";
            	
    		long time1 = System.nanoTime();
            	
    		for (int i=0; i<=points; i++) {
    			if (i>0) {
    				xArr += ", ";
    				yArr += ", ";
    			}
    			Double x = x0 + ((x1-x0)/points)*i;
    			parser.setVar(num, x); // подставляем значение переменной
    			Double y = expr.evaluate(); // вычисляем выражение
    			xArr += x.toString();
    			yArr += y.toString();
    		}
            	
    		long time2 = System.nanoTime();
            	
    		Integer parse_time = (int)((time1-time0)/1e6);
    		Integer calc_time = (int)((time2-time1)/1e6);
    		Integer total_time = (int)((time2-time0)/1e6);
            	        	
    		System.out.println("Parse time: "+parse_time.toString()
    								+"; Calc time: "+calc_time.toString());
            	
    		xArr += "]";
    		yArr += "]";
            	
    		result += "\"ok\", \"time\" : \""+total_time.toString()
    					+"\", \"x\" : \""+xArr+"\", \"y\" : \""+yArr+"\"}";
    	} catch (BadVarException e) {
    		result += "\"error\", \"err\" : \"badvar\"}";
    	} catch (Exception e) {
    		result += "\"error\", \"err\" : \"wrong\"}";
    	} finally {
    		out.print(result);
    	}
    }
    


    js-код для построения графика:

    window.getResult = function(func,variable,begin,end){
    	$('#draw').prop('disabled',true);
    	$('#info').text('Идет вычисление...');
    	$.post('/functions/calc',
    				{func: func, variable: variable, begin: begin, end: end},
    																function(data){
    		window.result = $.parseJSON(data);
    		if (result.result == 'error') {
    			if (result.err == 'wrong') $('#info').text('Ошибка в выражении');
    			if (result.err == 'badvar') $('#info').text('Имя переменной зарезервировано');
    			$('#draw').prop('disabled',false);
    			return;
    		}
    		
    		window.xArr = $.parseJSON(result.x);
    		result.y = result.y
    						.split('-Infinity').join('null')
    						.split('Infinity').join('null')
    						.split('NaN').join('null');
    		window.yArr = $.parseJSON(result.y);
    		window.data = [];
    		for (i=0; i<xArr.length; i++) { 
    			y = window.yArr[i];
    			window.data[i] = [window.xArr[i],window.yArr[i]]; 		
    		}
    		window.drawFunc();
    		var millis = result.time%1000;
    		var sec = (result.time-millis)/1000;
    		$('#info').text('Вычислено за '+sec+'.'+millis+' секунд');
    		$('#draw').prop('disabled',false);
    	});
    };
    
    window.drawFunc = function() {
    	var data_arr = [];
    	data_arr[0] = {data:window.data, color:'#f00'};
    	$.plot($('#graph'),data_arr);
    };
    


    Результат можно увидеть здесь (простые функции) и здесь (параметрические).

    Примечание: работает довольно медленно, так как сервер крутится на Raspberry Pi.
    Share post
    AdBlock has stolen the banner, but banners are not teeth — they will be back

    More
    Ads

    Comments 14

      0
      А можно узнать, в чем преимущества такого решения перед чисто клиентским, например, math.js?
        0
        На тот момент, когда я это реализовывал, я осваивал технологию Java сервлетов — и основной целью было написать что-либо для тренировки.
        На счет преимуществ:
        В одном плане чисто клиентское решение будет даже лучше — отсутствие лишней нагрузки на сервер.
        С другой стороны — в моей библиотеке есть реализация специальных функций (гамма, бета, функции Бесселя и Эйри) — не уверен, что найдутся js-либы, которые это умеют.
          0
          У вас гамма-функция вычисляется рекурсивно для больших аргументов, что негативно сказывается на использовании стека и времени работы. Возможно стоит применить формулу Стрирлинга, либо посмотреть, как она реализована в стандартных библиотеках (тот же GSL, например)
            0
            Спасибо за совет.

            От рекурсии избавился. Сделал так:

            public static double eulergamma(double x){
                if((x<=0.0)&&(Math.round(x)==x)){
                    System.out.print("\nargument of gamma cannot be zero or negative integer\n");
                    return Double.NaN;
                }
                //if(x>1.5) return (x-1)*eulergamma(x-1);
                //if(x<0.5) return eulergamma(x+1)/x;
                double mult = 1.0;
                while (x>1.5) {
                    mult *= x-1;
                    x -= 1;
                }
                while (x<0.5) {
                    mult /= x;
                    x += 1;
                }
              
                return mult*Math.exp(lngamma(x));
            }
            
              0
              Экспирементальным путем установил, что при x>180 функция возвращает Infinity, при x<-180 — positive or negative zero.

              Добавил это:

              if (x>180) return Double.POSITIVE_INFINITY;
              if (x<-180) {
                  int xIntAbs = (int)(-x);
                  int sign = 2*(xIntAbs%2)-1; 
                  return 0.0*sign;
              }
              
          +2
          Может быть не совсем в тему, но у у вас отсутствуют проверки на стороне сервера, в следствии чего ему вполне можно скормить подобный запрос:

          func=sin(1/x)&variable=y&begin='&end=0

          что вызовет некие неполадки на его стороне.
            +2
            И ещё в кучу к вышесказанному — построение графика функции
            sin(1/')
            сервер принимает и даже строит такой график, причем поведение его в этих случаях не совсем понятно. Скажем, если я пытаюсь отправить
            sin(1/'0)
            то ответом приходят 'NaN'-ы. А вот если отправить
            sin(1/-0)
            то график опять вполне успешно строится, как-бы откидывая все, что идет после единицы.
              0
              Пофиксил.
              А на счет ' — парсер воспринимает этот символ как пробел.
            0
            На тот момент, когда я это реализовывал, я осваивал технологию Java сервлетов — и основной целью было написать что-либо для тренировки.
            На счет преимуществ:
            В одном плане чисто клиентское решение будет даже лучше — отсутствие лишней нагрузки на сервер.
            С другой стороны — в моей библиотеке есть реализация специальных функций (гамма, бета, функции Бесселя и Эйри) — не уверен, что найдутся js-либы, которые это умеют.
              0
              Должны найтись, а если не найдутся, то можно перегнать java в js. Есть конвертеры.
              0
              Слишком много конкатенаций строк, советую использовать StringBuilder.
                0
                По-хорошему здесь можно использовать библиотеку GSON.
                Но на момент написания этого кода я про нее еще не знал.
                  0
                  Если уж взялись за Java EE, то для JSONа советую посмотреть на JSR353.
                    0
                    Почитал про JSR353 немного.
                    Но по-моему GSON на порядок удобнее — нужно просто создать классы для тех объектов, которые мы хотим перегнать в JSON (и добавить простые аннотации, если имена полей класса отличаются от нужных нам в JSONе) — и библиотека сделает все сама.

              Only users with full accounts can post comments. Log in, please.