Анимированная гистограмма: часть 1. Функционально-процедурный подход

Записи из этой темы
  1. Анимированная гистограмма: часть 1. Функционально-процедурный подход
  2. Анимированная гистограмма: часть 2. Создание плагина jQuery

Условие задачи: необходимо разместить на странице виджет, который изображает количество работ, разделенные по годам. (для справки: заказчик является видеооператором). Виджет должен состоять из колонок, причем каждая колонка — это определенной год (далее «значение X«), и высота колонки пропорциональна количеству работ для этого года (далее «значение Y«). Иными словами виджет представляет собой гистограмму. Дополнительно необходимо, чтобы в момент отображения этого виджета в зоне видимости окна браузера начинали подниматься указанные выше колонки до своих значений.

Хочу отметить, что в данном примере приводится лишь путь к решению данной задачи и демонстрируются и описываются встреченные преграды и способы по и преодолению. И ни в коем случае не стоит рассматривать как абсолютно универсальный способ решения этой задачи.

Как показывает практика, заказчик просит сделать такой же продукт, но обязательно с «перламутровыми пуговичками».

Для начала определимся с глобальными переменными и форматом того, как будут выглядеть исходные данные для виджета (соответствие значений X и Y). Я посчитал, что для этой цели лучше использовать объект (аналог ассоциированного массива в php, хеш-таблицы в Java или словаря в Python), а не просто массив. Так же для этой цели хорошо подойдет массив объектов.

var max_valueY, // максимальное значение Y
    DURING = 1000, // продолжительность времени, за которое должны выстроиться все колонки (в миллисекундах)
    SMOOTHNESS = 20, // сколько в секунду будет итераций повышения столбика (характеризует сглаженность анимации)
    dataArray = {
        '1998': 12,
        '1999': 22,
        '2000': 22,
        '2001': 23,
        '2002': 135,
        '2003': 20,
        '2004': 151,
        '2005': 52,
        '2006': 162
    };

Можно выделить два основных этапа формирования виджета:

  • Инициализация гистограммы — построение дерева DOM виджета в зависимости от количества данных (значений X)
  • Запуск анимации

Инициализация гистограммы

Для выполнения первого этапа требуется определиться с тем, как в HTML и CSS будет реализован виджет. Я выбрал следующую конструкцию:

  • контейнер для столбиков гистограммы — это блочно-строковые элементы (позволяет указывать размеры и разместить их в одну строку, а не в столбик как это происходит с блочными элементами);
  • столбики гистограммы — это подкрашенные блочные элементы с одинаковой высотой, независимо от значений X;
  • «занавеска» — это блочный элемент, который расположен над столбиком и будет закрывать его сверху пропорционально значению X.

Конкретная реализация виджета на HTML и CSS:

<div class="histogram_box">

    <!-- первая колонка: начало -->
    <div class="count_order_item">
        <div class="colomn">
            <div class="blind" data-index="значение X1">
                <div class="count">значение Y1</div>
            </div>
        </div>
        <div class="year">значение X1</div>
    </div>
    <!-- первая колонка: конец -->

    <!-- вторая колонка: начало -->
    <div class="count_order_item">
        <div class="colomn">
            <div class="blind" data-index="значение X2">
                <div class="count">значение Y2</div>
            </div>
        </div>
        <div class="year">значение X2</div>
    </div>
    <!-- вторая колонка: конец -->

    <!-- далее остальные остальные колонки -->
</div>
.histogram_box{
    margin: 10px 0 0 10px;
    text-align: center;
    padding-bottom: 20px;
}
.histogram_box .count_order_item{
    position: relative;
    display: inline-block;
    margin: 0 5px;
    font-family: 'Arial Narrow', Arial;
    font-size: 16px;
}
.histogram_box .count{
    color: #4c4744;
    line-height: 20px;
}
.histogram_box .year{    color: #c8022c;}
.histogram_box .colomn{
    background: #c00;
    width: 40px;
    height: 122px;
    margin: 5px auto;
}
.histogram_box .colomn .blind{
    background: #fff;
    padding-top: 100px;
}

Обращаю внимание на атрибут data-index у элемента .histogram_box > .count_order_item > .column > .blind. В этом атрибуте будет хранится значение X, то есть год.

Теперь необходимо, чтобы приведенный HTML код генерировался с помощью jQuery в зависимости от количества данных в переменной dataArray. Это реализовано в функции create_histogramm(), в которой также вычисляется максимальное значение Y из массива dataArray и это значение будет записано в глобальную переменную max_valueY.

/**
 * Создание html-макета для гистограммы
 */
function create_histogram(){
    var valueX;
    max_valueY = 0;
    for ( valueX in dataArray ){
        var html_column = '';
        max_valueY = ( max_valueY < dataArray[valueX] ) ? dataArray[valueX] : max_valueY;
        html_column += '<div class="count_order_item">';
        html_column += '<div class="column"><div class="blind" data-index="' + valueX + '"><div class="count">0</div></div></div>';
        html_column += '<div class="year">' + valueX + '</div>';
        html_column += '</div>';
        $('.histogram_box').append(html_column);
    }
}

Таким образом, и реализуется этап инициализация гистограммы.

Запуск анимации

Механизм плавного роста колонки будем использовать следующий. Сначала блок .blind будет иметь верхний внутренний отступ (paddingtop) равный максимально возможной высоте колонки, то есть видимая высота колонки будет равна нулевому значению. Затем через равные промежутки времени interval этот отступ будет уменьшаться на одинаковое значение iterator, пока видимая часть колонки .column не станет требуемой высоты. Длительность одной итерации interval будет зависеть от глобальной переменной SMOOTHNESS, которая характеризует плавность анимирования, то есть количество итераций за одну секунду. Чем выше значение SMOOTHNESS, тем плавнее будет «подниматься» колонка. А количество итераций будет зависеть еще и от продолжительности анимации DURING, так как колонка должна подняться до требуемого значения за DURING миллисекунд. То есть произведение количества итераций и значения interval должно быть равно значению глобальной переменной DURING.

Почему в представленном механизме изменяется именно padding-top у элемента? Внутри блока .blind будет содержаться значение Y соответствующей колонки (которое также будет меняться с каждой итерацией). И необходимо чтобы это значение было как можно ближе к вершине колонки.

Функции run_animation_histogram() запускает процесс анимации роста колонок гистограммы. В этой функции инициализируется длительность итерации interval. Затем для каждой колонки определяется значение iterator («размер» прироста колонки за каждую итерацию) и вызывается функция animate_column() с начальными параметрами для анимации колонки.

/**
 * Запуск анимации в гистограмме
 */
function run_animation_histogram(){
    var valueX,
        iterator,
        interval = Math.floor( 1000 / SMOOTHNESS );
    for ( valueX in dataArray ){
        var valueY = dataArray[valueX];
        iterator = Math.ceil( valueY / ( SMOOTHNESS * DURING  / 1000 ) );
        animate_column( valueX, 0, iterator, interval);
    }
}

В качестве входных параметров функции animate_column() являются:

  • значение X, по которому будет идентифицироваться нужная колонка (valueX);
  • значение Y, до которой колонка уже «доросла» в момент вызова функции (current);
  • «размер» прироста колонки за одну итерацию (iterator)
  • длительность итерации (interval).

Соответственно при первом вызове этой функции для какой-либо колонки значение current равно нулю, т.е. колонка находится начальном состоянии. Затем вычисляется значение current и значение padding-top колонки, которые станут в конце текущей итерации. Соответствующие значения будут присвоены блокам .blind и вложенному в него .count. После этого функция animate_column() будет вызвана с вычисленным значением current через интервал времени interval, с помощью служебной функции setTimeout.

Как только значение current превысит значение Y для текущей колонки, прекратится рекурсивный вызов функции animate_column(), что будет соответствовать окончанию анимации роста этой колонки.

/**
 * Анимирование отдельной колонки в гистограмме
 * @param  {string} valueX значение X
 * @param  {int} current  текущая "высота" на которую поднялась колонка
 * @param  {int} iterator шаг, с которым должна подниматься колонка
 * @param  {int} interval интервал времени, через который надо поднять колонку
 */
function animate_column( valueX, current, iterator, interval){
    var blind_padding_top;
    if ( current < dataArray[valueX] ){
        current += iterator;
        current = ( current > dataArray[valueX] ) ? dataArray[valueX] : current;
        blind_padding_top = Math.ceil( 100 * ( 1 - current / max_valueY ) );

        $('.count','.blind[data-index=' + valueX + ']').html(current);
        $('.blind[data-index='+valueX+']').css('paddingTop', blind_padding_top);
        setTimeout(
            function(){
                animate_column(valueX, current, iterator, interval);
            },
            interval
        );
    }
}

Теперь осталось вызвать функции инициализации и запуска анимации гистограммы. В приведенном примере будем периодически перезапускать гистограмму.

/**
 * Перезагрузка анимированной гистограммы
 */
function reload_histogram(){
    $('.histogram_box').empty();
    create_histogram();
    run_animation_histogram();
}

$(document).ready(function(){
    reload_histogram();
    setInterval(
        function(){
            reload_histogram();
        },
        DURING + 1000
    );
});

Демонстрация результата проделанной работы:

Недостатки данной реализации

  1. Использование глобальных переменных. Использование глобальных переменны — это зло для любого языка программирования. Использование глобальных переменных допускается только в ограниченных случаях, и то их лучше считать константами. В приведенном примере лишней глобальными переменными являются max_valueY и dataArray. Наличие этих переменных не позволяет разместить на странице несколько гистограмм с разными значениями.
  2. Функционально-процедурный подход. Этот подход считается морально-устаревшим, но до сих пор пользуется большой популярностью у начинающих программистов (и очень часто у продолжающих) ввиду простоты написания кода. Однако, данных подход усложняет понимание кода другими разработчиками. Также усложняется доработка и исправление ошибок.

Оформление данной функции в виде плагина jQuery позволит избежать эти недостатки. Это действие описано во второй части.

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *