/ Ваше резрешение экрана: px / Ваш Ip адрес:

О том, как рисовать кривые графики в стиле XKCD

3-11-2015, 01:36

Недавно я публиковал статью на Хабре про гигантный тюнер, и многих заинтересовали анимированные графики которые я использовал для иллюстрации звуковых волн, в том числе технология создания таких графиков. Поэтому в этой статье я поделюсь своим подходом и библиотечкой на Node.js которая поможет строить подобные графики.


Предыстория

 

Зачем делать графики кривыми?


Вообще, идея создания кривых графиков идет из академической культуры — не только российской, но и мировой.

Этот подход, когда даже довольно сложная научная информация иллюстрируется небрежными графиками является довольно распространенной практикой.

Именно на этом нюансе создаются комиксы, юмор которых базируется на простых зависимостях интерпретируемых в некоторой необычной манере:



Небрежность в графиках позволяет сместить внимание с количественной оценки, на качественную, что в свою очередь способствует лучшему восприятию новой информации.

Зачем писать скрипты для построения графиков?


Во-первых, когда подготавливается публикация где очень много исходных данных или где эти данные могут меняться во время подготовки, то лучше составлять и хранить графики в виде скриптов. В этом случае, если данные или результаты изменятся за время подготовки публикации, можно перестроить графики автоматически.

Во-вторых, трудно сказать, как тот или иной график будет выглядеть в публикации, поэтому часто приходится подгонять с учетом размеров полей, отступов и расположения текста и его выравнивание. Это проще всего делать если имеется скрипт и он позволяет путем изменения параметров перестроить график под новый вид. Наоборот, если график был сделан без скрипта в каком-то редакторе, то такие манипуляции становятся затратными по времени.

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

Почему Node.js?


Существует много библиотек для построение графиков, в том числе с эффектом XKCD, есть расширения для matplotlib и специальный пакет для R. Тем не менее, javascript имеет ряд преимуществ.

Для javascript доступен довольной удобный браузерный Canvas и Node.js-библиотеки которые реализуют это поведение. В свою очередь, скрипт написанный для Canvas можно воспроизвести в браузере, что позволяет, например, отображать данные на сайте динамически. Так же Canvas удобен для отладки анимации в браузере, т.к. отрисовка происходит фактически на лету. Имея скрипт отрисовки на Node.js можно задействовать пакет GIFEncoder, который позволяет очень просто создать анимированный ролик.

Добавление искривлений


Внешний вид графиков в стиле XKCD можно получить с помощью добавления случайных смещений. Но эти смещения должны добавляться не в каждой точке, иначе просто будет расплывчатый график, а с некоторым шагом.



Поэтому, любая линия, которую требуется отрисовать, должна разбиваться, а уже узловые точки — смещаться на некоторую случайную величину. Т.к. входящая линия может содержать либо слишком маленькие, либо слишком большие участки, то требуется алгоритм который бы объединял слишком маленькие в большие, и наоборот разбивал бы большие участки на маленькие.

Описанное поведение может быть реализовано следующим образом:

    self.replot = function(line, step, radius){
        var accuracy = 0.25;
        if(line.length < 2) return [];
        var replottedLine = [];
        var beginning = line[0];
        replottedLine.push(beginning);
        for(var i = 1; i < line.length; i++){
            var point = line[i];
            var dx = point.x - beginning.x;
            var dy = point.y - beginning.y;
            var d = Math.sqrt(dx*dx+dy*dy);
            if(d < step * (1 - accuracy) && (i + 1 < line.length)){
                // too short
                continue;
            }
            if(d > step * (1 + accuracy)){
                // too long
                var n = Math.ceil(d / step);
                for(var j = 1; j < n; j++){
                    replottedLine.push({
                        x: beginning.x + dx * j / n,
                        y: beginning.y + dy * j / n
                    });
                }
            }
            replottedLine.push(point);
            beginning = point;
        };
        for(var i = 1; i < replottedLine.length; i++){
            var point = replottedLine[i];
            replottedLine[i].x = point.x + radius * (self.random() - 0.5);
            replottedLine[i].y = point.y + radius * (self.random() - 0.5);
        };
        return replottedLine;
    };



Результат такой обработки:



Т.к. случайные смещения делают ломаными даже самые гладкие графики (а синусоида это идеал гладкости), то на случайных смещениях останавливаться нельзя — необходимо вернуть потерянную гладкость. Один из путей возвращения гладкости это использование квадратичных кривых вместо прямых отрезков.

Метод quadraticCurveTo из Canvas представляет отрисовку с достаточной для наших задач гладкостью, но при этом требует вспомогательные узлы. Эти узлы могу быть рассчитаны на основе опорных точек полученных на предыдущем шаге:

    ctx.beginPath();
    ctx.moveTo(replottedLine[0].x, replottedLine[0].y);
    for(var i = 1; i < replottedLine.length - 2; i ++){
        var point = replottedLine[i];
        var nextPoint = replottedLine[i+1];
        var xc = (point.x + nextPoint.x) / 2;
        var yc = (point.y + nextPoint.y) / 2;
        ctx.quadraticCurveTo(point.x, point.y, xc, yc);
    }
    ctx.quadraticCurveTo(replottedLine[i].x, replottedLine[i].y, replottedLine[i+1].x,replottedLine[i+1].y);
    ctx.stroke();



Полученная сглаженная линия как раз и будет соответствовать небрежному начертанию:



Библиотечка Clumsy



На основе приведенных алгоритмов, я построил небольшую библиотеку. В основе лежит класс-обертка Clumsy, который реализует нужное поведение с помощью объекта Canvas.

В случае Node.js процесс инициализации выглядит примерно так:

    var Canvas = require('canvas');
    var Clumsy = require('clumsy');
    var canvas = new Canvas(800, 600);
    var clumsy = new Clumsy(canvas);



Основные методы класса, необходимы для отрисовки простейшего графика:

    range(xa, xb, ya, yb); // задает границы сетки графика
    padding(size); // размер отступа в пикселах
    draw(line); // отрисовывает линию
    axis(axis, a, b); // отрисовывает ось
    clear(color); // очищает canvas заданным цветом
    tabulate(a, b, step, cb); // вспомогательный метод для табулирования данных





Как это работает можно продемонстрировать на примере синуса:

    clumsy.font('24px VoronovFont');
    clumsy.padding(100);
    clumsy.range(0, 7, 2, 2);
    var sine = clumsy.tabulate(0, 2*Math.PI, 0.01, Math.sin);
    clumsy.draw(sine);
    clumsy.axis('x', 0, 7, 0.5);
    clumsy.axis('y', -2, 2, 0.5);
    clumsy.fillTextAtCenter("Синус", 400, 50);





Анимация



Добиться движущегося изображения на Canvas'е в браузере довольно просто, достаточно обернуть алгоритм отрисовки в функцию и передать в setInterval. Такой подход удобен в первую очередь для отладки, т.к. результат наблюдается непосредственно. Что же касается генерации готового gif'а на Node.js, то в этом случае можно воспользоваться библиотекой GIFEncoder.

Для примера, возьмем спираль Архимеда, которую заставим вращаться со скоростью pi радиан в секунду.
Когда требуется анимировать некоторый график удобнее всего сделать отдельный файл отвечающий исключительно за отрисовку, и отдельно файлы настраивающие параметры анимации — fps, длительность ролика, и т.п. Назовем скрипт отрисовки spiral.js и создадим в нем функцию Spiral:

    function Spiral(clumsy, phase){
        clumsy.clear('white');
        clumsy.padding(100);
        clumsy.range(-2, 2, -2, 2);
        clumsy.radius = 3;
        var spiral = clumsy.tabulate(0, 3, 0.01, function(t){
            var r = 0.5 * t;
            return {
                x: r * Math.cos(2 * Math.PI * t + phase),
                y: r * Math.sin(2 * Math.PI * t + phase)
            };
        })
        clumsy.draw(spiral);
        clumsy.axis('x', -2, 2, 0.5);
        clumsy.axis('y', -2, 2, 0.5);
        clumsy.fillTextAtCenter('Спираль', clumsy.canvas.width/2, 50);
    }
    // Костыль для предотвращения экспорта в браузере
    if(typeof module != 'undefined' && module.exports){
        module.exports = Spiral;
    }



Затем можно просмотреть результат в браузере, сделав отладочную страницу:

    <!DOCUMENT html>
    <script src="https://rawgit.com/kreshikhin/clumsy/master/clumsy.js"></script>
    <link rel="stylesheet" type="text/css" href="http://webfonts.ru/import/voronov.css"></link>
    <canvas id="canvas" width=600 height=600>
    <script src="spiral.js"></script>
    <script>
        var canvas = document.getElementById('canvas');
        var clumsy = new Clumsy(canvas);
        var phase = 0;
        setInterval(function(){
            // Фиксированный seed предотвращает "дрожание" графика
            clumsy.seed(123);
            Spiral(clumsy, phase);
            phase += Math.PI / 10;
        }, 50);
    </script>



Отладка в браузере удобна тем, что результат появляется сразу же. Т.к. не требуется время на генерацию кадров и сжатие в формат GIF. Что может занять несколько минут. Сохранив страницу в .html формате и открыв в браузере мы должны увидеть на Canvas вращающаяся спираль:



Когда график отлажен, можно используя тот же файл spiral.js создать скрипт для генерации GIF-файла:

    var Canvas = require('canvas');
    var GIFEncoder = require('gifencoder');
    var Clumsy = require('clumsy');
    var helpers = require('clumsy/helpers');
    var Spiral = require('./spiral.js');
    var canvas = new Canvas(600, 600);
    var clumsy = new Clumsy(canvas);
    var encoder = helpers.prepareEncoder(GIFEncoder, canvas);
    var phase = 0;
    var n = 10;
    encoder.start();
    for(var i = 0; i < n; i++){
        // Фиксированный seed предотвращает "дрожание" графика
        clumsy.seed(123);
        Spiral(clumsy, phase);
        phase += 2 * Math.PI / n;
        encoder.addFrame(clumsy.ctx);
    };
    encoder.finish();



Абсолютно аналогичным образом я создавал графики для иллюстрации явления стоячей волны:



Исходный код scituner-standing-group.js





Исходный код scituner-standing-phase.js



Заключение


Итак, используя такую бесхитростную обертку над Canvas можно добиться довольно оригинальной отрисовки графиков в стиле XKCD. В общем это и была главная цель создания библиотечки.

Она не универсальна, но если необходимо построить довольно простой график в стиле XKCD, то с этой задачей она справляется более чем хорошо. Дополнительные возможности можно реализовывать самостоятельно используя возможности HTML5 Canvas.



Теги:

Вы находитесь в категории Вебмастеру. Материл предоставлен для пользователей в ознакомительных целях. Все новости публикуются на русском языке и с русским интерфейсом (rus). Мы предоставляем только те файлы, которые востребованы и интересны на наш взгляд именно для посетителей нашего сайта и следовательно рекламируем тот или иной контент на русском. Опробовав продукт, каждый пользователь обязан приобрести лицензию, если такова требуется.



admin Категория: Вебмастеру

Важно: если не отображается страница новости или ее часть просьба отключить блокировщик рекламы. Проблема может возникать из-за того что скрипт сайта реагирует на программы подобного типа.
Также Вы можете сообщить о проблеме или ошибке и ознакомиться с ответами пользователей.
Добавление комментария
  • winkwinkedsmileam
    belayfeelfellowlaughing
    lollovenorecourse
    requestsadtonguewassat
    cryingwhatbullyangry
Введите код: *
Кликните на изображение чтобы обновить код, если он неразборчив

Популярное...
Кто на сайте
    Всего на сайте: 1
    Пользователей: 0
    Гостей: 1
На заметку...

Чтобы смотреть все статьи перейдите в категорию "Интересное" по ссылке "Интересно знать" (вверху)
Облако тегов
Интересное