Проверено

VDS-хостинг - именно на этом хостинге работает наш сайт.
Sape - биржа временных ссылок, доход 80р/день.
Trustlink - биржа временных ссылок, доход 60р/день.
Telderi - здесь покупаю сайты для дальнейшего заработка.

Партнеры

Вы можете найти используя, костюмы для аниматоров.

Привлекательная анимированная круглая диаграмма

В данном уроке мы разберем, как построить привлекательную круговую диаграмму, которая обладает интерактивными функциями на основе HTML5. Еще недавно такие задачи можно было решать только с использованием Flash. Но теперь, благодаря появлению элемента HTML5 canvas мы можем создавать чудесные анимационные эффекты с использованием только javascript, CSS и математики!

Исходники
Прикреплённые файлы:
Файл: demo.zip
Размер: [9,04 Kb] (забрали: 15 раз)


Шаг 1. Создаем разметку

Вот разметка нашей демонстрационной страницы:

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
<head>

<title>Привлекательная анимированная диаграмма | HTML5 и jQuery | Демонстрация для сайта RUSELLER.COM</title>
<meta http-equiv="Content-Type" content="text/html;charset=utf-8" >

</head>
<body>

<div id="container">

  <div class="wideBox">
    <h1>Продажи штукуевин в 2010</h1>
    <p>Нажмите на цветном секторе в диаграмме или на строке в таблице, чтобы выдвинуть сектор!</a></p>
  </div>

  <canvas id="chart" width="600" height="500"></canvas>

  <table id="chartData">

    <tr>
      <th>Штукуевина</th><th>Продажи ($)</th>
     </tr>

    <tr style="color: #0DA068">
      <td>СуперШтука</td><td>1862.12</td>
    </tr>

    <tr style="color: #194E9C">
      <td>МегаШтука</td><td>1316.00</td>
    </tr>

    <tr style="color: #ED9C13">
      <td>ГиперШтука</td><td>712.49</td>
    </tr>

    <tr style="color: #ED5713">
      <td>ЧудоШтука</td><td>3236.27</td>
    </tr>

    <tr style="color: #057249">
      <td>МикроШтука</td><td>6122.06</td>
    </tr>

    <tr style="color: #5F91DC">
      <td>НаноШтука</td><td>128.11</td>
    </tr>

    <tr style="color: #F88E5D">
      <td>Модная Штука</td><td>245.55</td>
    </tr>
  </table>

</div>

</body>
</html>


Разметка очень простая. Она содержит:
div container, в котором размещен контент для центрирования на странице
Элемент HTML5 canvas для круговой диаграммы
Элемент table, который содержит данные для диаграммы
Заголовок страницы

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

Шаг 2. Создаем CSS

Теперь, когда у нас есть основа HTML страницы, зададим стили CSS для различных элементов:

<style>

body {
  background: #fff;
  color: #333;
  font-family: "Trebuchet MS", Verdana, Arial, Helvetica, sans-serif;
  font-size: 0.9em;
  padding: 40px;
}

.wideBox {
  clear: both;
  text-align: center;
  margin-bottom: 50px;
  padding: 10px;
  background: #ebedf2;
  border: 1px solid #333;
  line-height: 80%;
}

#container {
  width: 900px;
  margin: 0 auto;
}

#chart, #chartData {
  border: 1px solid #333;
  background: #ebedf2 url("images/gradient.png") repeat-x 0 0;
}

#chart {
  display: block;
  margin: 0 0 50px 0;
  float: left;
  cursor: pointer;
}

#chartData {
  width: 200px;
  margin: 0 40px 0 0;
  float: right;
  border-collapse: collapse;
  box-shadow: 0 0 1em rgba(0, 0, 0, 0.5);
  -moz-box-shadow: 0 0 1em rgba(0, 0, 0, 0.5);
  -webkit-box-shadow: 0 0 1em rgba(0, 0, 0, 0.5);
  background-position: 0 -100px;
}

#chartData th, #chartData td {
  padding: 0.5em;
  border: 1px dotted #666;
  text-align: left;
}

#chartData th {
  border-bottom: 2px solid #333;
  text-transform: uppercase;
}

#chartData td {
  cursor: pointer;
}

#chartData td.highlight {
  background: #e8e8e8;
}

#chartData tr:hover td {
  background: #f0f0f0;
}

</style>


Здесь тоже нет сюрпризов. CSS содержит правила для страницы, прямоугольников заголовка и нижнего колонтитула, контейнера, элементов #chart canvas и #chartData table.

Отметим пару моментов:
Элементы #chart и #chartData получают в качестве фона нежный градиент, создаваемый с помощью изображения gradient.png (оно входит в состав исходников). Да, для элемента canvas можно устанавливать изображения в качестве фона!
Мы используем свойство CSS3 box-shadow (и его эквиваленты для различных браузеров), чтобы установить тень для таблицы данных. (Хотя возможно добавить тень и для элемента canvas, но практика показала, что в этом случае анимация диаграммы существенно замедляется в браузерах WebKit.)



Шаг 3. Включаем jQuery и библиотеку ExplorerCanvas

Теперь можно приступать к написанию javascript кода. сначала включим две библиотеки:


<!--[if IE]>



jQuery не нуждается в представлении.

ExplorerCanvas. Internet Explorer, конечно же не поддерживает элемент canvas. К счастью, несколько отличных программистов создали ExplorerCanvas, библиотеку, которая эмулирует большинство методов и свойств элемента canvas с помощью возможностей IE SVG. Не все так хорошо, как хотелось бы, но для наших целей будет достаточно. Так как только IE нуждается в данной библиотеке, то мы используем условный комментарий для ее загрузки только в IE.

Обе библиотеки подгружаются с удаленных хранилищ, но вы можете скопировать их на локальный сервер и загружать с него.
Шаг 4. Создаем основную функцию, раздел конфигурации и полезные перменные

Мы заключим наш код в одну основную функцию pieChart(). Таким образом, все функции и переменные, относящиеся к диаграмме, будут сконцентрированны в одном месте и не будут загрязнять глобальное пространство имен. Для вызова функции pieChart() мы будем использовать jQuery после загрузки DOM.

Начнем с размещения переменных конфигурации в функции pieChart():

// Код выполняется когда DOM готова
$( pieChart );

function pieChart() {

  // Конфигурация
  var chartSizePercent = 55;                        // Радиус диаграммы, выраженный в процентах от размеров области рисования
  var sliceBorderWidth = 1;                         // Ширина (в пискселях) границы вокруг каждого сектора
  var sliceBorderStyle = "#fff";                    // Цвет границы вокруг каждого сектора
  var sliceGradientColour = "#ddd";                 // Цвет, который используется с одного конца диаграммы для создания градиента
  var maxPullOutDistance = 25;                      // Насколько далеко будет выдвигаться сектор из диаграммы
  var pullOutFrameStep = 4;                         // На сколько пикселей перемещается сектор в каждом кадре анимации
  var pullOutFrameInterval = 40;                    // Сколько ms проходит между кадрами
  var pullOutLabelPadding = 65;                     // Отступ между выдвинутым сектором и его меткой
  var pullOutLabelFont = "bold 16px 'Trebuchet MS', Verdana, sans-serif";  // Шрифт метки выдвинутого сектора
  var pullOutValueFont = "bold 12px 'Trebuchet MS', Verdana, sans-serif";  // Шрифт значения выдвинутого сектора
  var pullOutValuePrefix = "$";                     // Префикс значения выдвинутого сектора
  var pullOutShadowColour = "rgba( 0, 0, 0, .5 )";  // Цвет тени выдвинутого сектора
  var pullOutShadowOffsetX = 5;                     // Смещение по оси X (в пикселях) тени выдвинутого сектора
  var pullOutShadowOffsetY = 5;                     // Смещение по оси Y (в пикселях) тени выдвинутого сектора
  var pullOutShadowBlur = 5;                        // Насколько сильно размыта тень выдвинутого сектора
  var pullOutBorderWidth = 2;                       // Ширина (в пикселях) границы выдвинутого сектора
  var pullOutBorderStyle = "#333";                  // Цвет границы выдвинутого сектора
  var chartStartAngle = -.5 * Math.PI;              // Начало диаграммы на 12 часов, а не на 3-х

  // Объявдение некоторых перменных для диаграммы
  var canvas;                       // Область рисования на странице
  var currentPullOutSlice = -1;     // Сектор, который выдвинут в текущий момент(-1 = нет выдвинутого сектора)
  var currentPullOutDistance = 0;   // На сколько пикселей смещен текущий выдвигаемый сектор в ходе анимации
  var animationId = 0;              // ID интервала анимации, созданный с помощью setInterval()
  var chartData = [];               // Данные диаграммы (метки, значения, углы)
  var chartColours = [];            // Цвета диаграммы (получены из таблицы HTML)
  var totalValue = 0;               // Сумма всех значений в диаграмме
  var canvasWidth;                  // Ширина области рисования
  var canvasHeight;                 // Высота области рисования
  var centreX;                      // Координата X центра диаграммы на области рисования
  var centreY;                      // Координата Y центра диаграммы на области рисования
  var chartRadius;                  // Радиус диаграммы в пикселях

  // Инициализируем данные и рисуем диаграмму
  init();


Большинство из этих строк имеют очевидное значение в соответствии с комментарием. Несколько важных переменных разберем более тщательно:

chartSizePercent
Для того, чтобы выдвигать сектор и выводить метку для него нужно иметь достаточно пространства. Реальный размер диаграммы будет меньше, чем область рисования. В нашем случае 55% является достаточной величиной.


chartStartAngle
По умолчанию углы в javascript, как и в большинстве языков программирования, задаются в радианах, при этом 0 радиан соответствует положению на 3 часа.Так как мы хотим начать отсчет с 12 часов, нужно установить смещение на π/2 радиан (четверть круга) для всех углов в коде. На рисунке ниже приводится подробное объяснение.


currentPullOutSlice и currentPullOutDistance
Так как мы планируем анимировать выдвижение сектора из диаграммы, то данные переменные нужны для отслеживания анимации. currentPullOutSlice содержат указание на то, какой сектор выдвигается из диаграммы (величина -1 означает, что никакой сектор из диаграммы не выдвинут), а currentPullOutDistance задает дистанцию, на которую выдвигается сектор.


animationId
Данная переменная содержит значение, которое возвращает функция setInterval(), когда мы создаем анимацию. Это числовой идентификатор, который мы можем передавать функции clearInterval(), когда надо завершать анимацию.


chartData
Данный массив используется для хранения данных каждого сектора в диаграмме, включая метку и значение (которые получены из таблицы HTML), начальный и конечный угол.


chartColours
Массив, который содержит цвета для секторов. Значения также получаются из таблицы HTML.


init()
Вызывает функцию init(), которая устанавливает диаграмму и запускает остальные процессы.




Шаг 5. Инициализируем диаграмму

Теперь мы готовы вывести диаграмму. Давайте разберем функцию init():

/**
* Устанавливаем для диаграммы данные и цвета, а также устанавливаем обработчики события click
* для диаграммы и таблицы. Рисуем диаграмму.
*/

function init() {

  // Получаем область рисования на странице
  canvas = document.getElementById('chart');

  // Выходим, если браузер не имеет возможности рисовать
  if ( typeof canvas.getContext === 'undefined' ) return;

  // Инициализуем некоторые свойства области рисования и диаграммы
  canvasWidth = canvas.width;
  canvasHeight = canvas.height;
  centreX = canvasWidth / 2;
  centreY = canvasHeight / 2;
  chartRadius = Math.min( canvasWidth, canvasHeight ) / 2 * ( chartSizePercent / 100 );

  // Получаем данные из таблицы
  // и устанавливаем обработчики события click для ячеек таблицы
  
  var currentRow = -1;
  var currentCell = 0;

  $('#chartData td').each( function() {
    currentCell++;
    if ( currentCell % 2 != 0 ) {
      currentRow++;
      chartData[currentRow] = [];
      chartData[currentRow]['label'] = $(this).text();
    } else {
     var value = parseFloat($(this).text());
     totalValue += value;
     value = value.toFixed(2);
     chartData[currentRow]['value'] = value;
    }

    // Сохраняем индекс сектора в ячейке и привязываем к ней обработчик события click
    $(this).data( 'slice', currentRow );
    $(this).click( handleTableClick );

    // Получаем и сохраняем цвет ячейки
    if ( rgb = $(this).css('color').match( /rgb((d+), (d+), (d+)/) ) {
      chartColours[currentRow] = [ rgb[1], rgb[2], rgb[3] ];
    } else if ( hex = $(this).css('color').match(/#([a-fA-F0-9]{2})([a-fA-F0-9]{2})([a-fA-F0-9]{2})/) ) {
      chartColours[currentRow] = [ parseInt(hex[1],16) ,parseInt(hex[2],16), parseInt(hex[3], 16) ];
    } else {
      alert( "Ошибка: Цвет не может быть определен! Пожалуйста, задайте таблицу цветов в формате '#xxxxxx'" );
      return;
    }

  } );

  // Теперь вычисляем и сохраняем начальный и конечный угол каждого сектора в диаграмме

  var currentPos = 0; // Текущая позиция сектора (от 0 до 1)

  for ( var slice in chartData ) {
    chartData[slice]['startAngle'] = 2 * Math.PI * currentPos;
    chartData[slice]['endAngle'] = 2 * Math.PI * ( currentPos + ( chartData[slice]['value'] / totalValue ) );
    currentPos += chartData[slice]['value'] / totalValue;
  }

  // Все готово! Теперь выводим диаграмму и добавляем обработчик события click к ней
  drawChart();
  $('#chart').click ( handleChartClick );
}


Заметьте, что функция init(), так же как и все остальные функции нашего урока, должна располагаться внутри функции pieChart(). Внутренние функции будут иметь доступ к переменным, определенным во внешней функции.

Функция init() ужасно длинная. Вот что она делает:

Получаем элемент canvas
Сначала получаем элемент "#chart" canvas со страницы и сохраняем его в объекте canvas. Мы будем выполнять все рисование через данный объект.

Проверяем поддержку элемента canvas в браузере
Прежде, чем начать что либо делать, надо проверить факт, что браузер поддерживает элемент HTML5 canvas. Для этого мы проверяем, что объект canvas содержит метод getContext() — часто используемый метод элемента. Если его нет, то браузер вероятно не поддерживает элемент canvas, значит следует прервать выполнение функции.

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

Получаем данные из таблицы
Мы используем селектор jQuery $('#chartData td') для выделения всех данных в таблице. Затем мы можем пройти циклом по всем ячейкам с помощью метода jQuery each(). Для каждой ячейки мы определяем ее метку (например, "СуперШтука") или значение (например, "1862.12"), в зависимости от того, в какой колонке находится величина. Затем мы сохраняем содержание ячейки под ключами 'label' или 'value' в ассоциированном массиве, который помещается в массив chartData.

Сохраняем индекс сектора с каждой ячейкой таблицы, и назначаем обработчик события click ячейке
При прохождении по ячейкам таблицы мы сохраняем текущий индекс строки (соответствует индексу сектора) в ключе 'slice' в объекте jQuery, который содержит ячейку таблицы. Для этого используется метод jQuery data(). Таким образом мы можем легко определить, какому сектору соответствует ячейка, елси на ней произойдет нажатие кнопки мыши. Мы также назначаем обработчик события click функцию handleTableClick()ячейке, таким образом, если на ячейке будет нажата кнопка мыши, то мы можем правильно анимировать диаграмму.

Получаем цвет ячейки и сохраняем его в массиве chartColours
Мы используем jQuery для того, чтобы получить цвет ячейки из свойства CSS color. Затем мы сохраняем цвет в массиве chartColors как трехэлементный массив , который содержит значения для красного, синего и зеленого уровней (в десятичном виде).

Большинство браузеров возвращают цвет элемента в формате "rgb(r, g, b)". Однако, некоторые браузеры (*кхе* IE *кхе*) просто возвращает цвет в том формате, который был задан в CSS (например, "#RRGGBB"). Таким образом, наш код использует регулярные выражения для проверки обоих сценариев.

Вычисляем и сохраняем начальный и конечный угол для каждого сектора
Нам нужно знать, с каких углов начинается и заканчивается сектор, практически во всем коде. Поэтому, предварительный расчет и хранения данных величин в элементах 'startAngle' и 'endAngle' ассоциированного массива внутри массива chartData является очень нужным действием. Для вычислений используется цикл по всем секторам с использованием перменной currentPos для сохранения промежуточного итога как отношения к общему итогу (между 0 и 1). Затем мы можем умножать промежуточный итог на 2π радиан (полный круг), чтобы получить начальный и конечный углы сектора.

Углы сектора сохраняются в chartData в значении от 0 до 2π (от 3-х часов до 3-х часов) . Нужно сместить данные углы с помощью перменной chartStartAngle при рисовании сектора,так как точка отсчета находится на 12 часов.

Рисуем диаграмму и присоединяем обработчик события click к элементу canvas
В завершении функция init() вызывает фукнцию drawChart() для вывода диаграммы на экран. А также назначает обработчик события click функцию handleChartClick()элементу canvas, так что если нажать кнопку мыши на диаграмме, то сектор либо выдвинется либо встанет на место в зависимости от условий.

Шаг 6. Пишем обработчик события click для диаграммы



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

Вот код функции:

/**
* Обрабатываем нажатие кнопки мыши в области диаграммы.
*
* Если нажатие произошло на секторе, переключаем его положение (задвинут/выдвинут).
* Если нажатие произошло вне области диагрммы, то задвигаем все сектора на место.
*
* @param Event Событие click
*/

function handleChartClick ( clickEvent ) {

  // Получаем положение курсора в момент нажатия кнопки мыши, по отношению к области рисования
  var mouseX = clickEvent.pageX - this.offsetLeft;
  var mouseY = clickEvent.pageY - this.offsetTop;

  // Кнопку мыши нажали внутри диаграммы?
  var xFromCentre = mouseX - centreX;
  var yFromCentre = mouseY - centreY;
  var distanceFromCentre = Math.sqrt( Math.pow( Math.abs( xFromCentre ), 2 ) + Math.pow( Math.abs( yFromCentre ), 2 ) );

  if ( distanceFromCentre <= chartRadius ) {

    // Да, кнопку мыши нажали внутри длиаграммы.
    // Ищем сектор, в котором была нажата кнопка мыши.
// Определяем угол по отношению к центру диаграммы.

    var clickAngle = Math.atan2( yFromCentre, xFromCentre ) - chartStartAngle;
    if ( clickAngle < 0 ) clickAngle = 2 * Math.PI + clickAngle;
                
    for ( var slice in chartData ) {
      if ( clickAngle >= chartData[slice]['startAngle'] && clickAngle <= chartData[slice]['endAngle'] ) {

        // Сектор найден. Выдвигаем его или задвигаем, в соответствиис текущим положением.
        toggleSlice ( slice );
        return;
      }
    }
  }

  // Должно быть пользователь нажал кнопку мыши вне диаграммы. Нужно задвинуть все сектора на место.
  pushIn();
}


Как и все обработчики событий jQuery, функция handleChartClick() должна принимать в качестве аргумента объект jQuery Event. Объект Event содержит информацию о событии click, включая координаты точки, где произошло событие.

Функция выполняет следующие операции:

Получает координаты положения курсора мыши в момент нажатия кнопки
Координаты точки, в которой находился курсор мыши в момент осбытия доступны через свойства pageX и pageY объекта Event. Однако, данные координаты соотнесенысо страницей, то есть нам надо вычесть координаты верхнего левого угла элемента canvas, чтобы получить положение соотнесенное с областью рисования. Это очень просто, потому что this в обработчике события click ссылается на элемент, в котором произошло событие (в нашем случае на элемент canvas). Мы можем затем получить положение верхнего левого угла с помощью offsetLeft и offsetTop.

Определяем, было ли сделано нажатие кнопки мыши внутри диаграммы
Теперь мы знаем в какой части области рисования произошло нажатие кнопки мыши и нужно определить, попадает ли данная точка в радиус диаграммы. С помощью теоремы Пифагора опредлеяем дистанцию от данной точко до центра ддиаграммы.

Находим на какой сектор нажали
Предполагая, что нажаите произошло в диаграмме, определяем на каком секторе произошло нажатие. Для этого нужно вычислить угол положения точки нажатия кнопки мыши по отношению к оси Х диаграммы с помощью функции atan2(). Значение chartStartAngle вычитается из вычисленной величины, так как диаграмма провернута относительно своего центра. Так как atan2() может вернуть отрицательное значение, нужно прибавить к нему 2π в этом случае, таким образом мы получим положительное значение в диапазоне от 0 до 2π. Теперь мы можем пройти циклом по секторам, до тех пор пока не найдем тот, чьи начальные и конечные значения углов соответствуют углу точки положения курсора во время события. Это нужный сектор.

Переключаем положение сектора
Теперь мы знаем, в каком секторе была нажата кнопка мыши, и можно вызывать функцию toggleSlice(), передав ей индекс сектора. Данная функция запускает процесс перемещения сектора в зависимости от его текущего положения.
Если пользователь нажал на кнопку мыши вне пределов диаграммы, то надо задвинуть все сектора на место
Если пользователь нажал кнопку мыши вне пределов диаграммы, то отличным решением будет задвинуть все сектора на место. Для этого мы вызываем функцию pushIn(), которая задвигает все сектора обратно.

Шаг 7. Пишем обработчик события click для таблицы

Обработчик события click для таблицы вызывается в том случае, если пользователь нажал кнопку мыши на ячейке таблицы. В этом случае надо переключить положение соответствующего сектора:

/**
* Обрабатываем событие click в области таблицы.
*
* Возвращает номер сектора из данных jQuery, сохраненных в
* нажатой ячейке, затем переключаем сектор.
*
* @param Event Событие click
*/

function handleTableClick ( clickEvent ) {
  var slice = $(this).data('slice');
  toggleSlice ( slice );
}


Обработчик события click для таблицы значительно проще обработчика для диаграммы! В функции init() мы сохраняли индекс сектора для каждой ячейки таблицы с помощью метода jQuery data(). Теперь легко определить, какой сектор нужно переключить с помощью простого вызова $(this).data('slice').
Шаг 8. Пишем функцию для переключения положения сектора

Данная функция вызывается из двух обработчиков события. Оно должна либо выдвинуть сектор, либо задвинуть его на место, в зависимости от текущего положения:

/**
* Задвигаем/выдвигаем сектор.
*
* Если сектор выдвинут - задвигаем его. И наоборот.
*
* @param Number Индекс сектора (между 0 и количеством секторов - 1)
*/

function toggleSlice ( slice ) {
  if ( slice == currentPullOutSlice ) {
    pushIn();
  } else {
    startPullOut ( slice );
  }
}


Функция не выполняет реальных операций по движению сектора, вместо этого она вызывает функции pushIn() и startPullOut() в зависимости от положения сектора.

Шаг 9. Cоздаем функцию для выдвижения сектора



Когда сектор выдвигается, мы будем анимировать движение. Для этого будет использоваться функция javascript setInterval() для вывода кадра анимации каждые несколько миллисекунд.

Функция startPullOut() запускает анимацию. Она использует setInterval() для вызова функции анимации animatePullOut(), а также выделяет соответствующую строку в таблице:

/**
* Запускаем выдвижение сектора из диаграммы.
*
* @param Number Индекс сектора (между 0 и количеством секторов - 1)
*/

function startPullOut ( slice ) {

  // Выходим, если сектор уже выдвинут
  if ( currentPullOutSlice == slice ) return;

  // Записываем сектор, который надо выдвинуть. Очищаем предыдущие анимации. Запускаем анимацию.
  currentPullOutSlice = slice;
  currentPullOutDistance = 0;
  clearInterval( animationId );
  animationId = setInterval( function() { animatePullOut( slice ); }, pullOutFrameInterval );

  // Выделяем соответствующую строку в таблице
  $('#chartData td').removeClass('highlight');
  var labelCell = $('#chartData td:eq(' + (slice*2) + ')');
  var valueCell = $('#chartData td:eq(' + (slice*2+1) + ')');
  labelCell.addClass('highlight');
  valueCell.addClass('highlight');
}


Несколько замечаний по данной функции:
При вызове функции setInterval() в нее передается анонимная функция, которая преобразуется в вызов animatePullOut( slice ). Это прекрасный пример замыкания, так как анонимная функция имеет доступ к переменной (slice) в контексте окружающей функции. Таким образом обеспечивается доступ функции setInterval() к функции animatePullOut() и значению переменной slice.
Функции setInterval()передается интервал в миллисекундах с помощью переменной pullOutFrameInterval. Поэтому setInterval() будет вызывать animatePullOut() каждые pullOutFrameInterval миллисекунд.
setInterval() возвращает ID интервала, который сохраняется в переменной animationId. Мы можем затем вызвать clearInterval() с указанием ID тогда, когда потребуется остановить анимацию.
Для выделения строки сначала мы удаляем класс 'highlight' у все ячеек таблицы, затем мы используем селектор jQuery :eq()для того, чтобы найти 2 ячейки в целевой строке и присваиваем им класс 'highlight'.
Шаг 10. Создаем функцию для анимации эффекта выдвижения

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

/**
* Рисуем кадр анимации выдвижения.
*
* @param Number Индекс сектора, который выдвигается
*/

function animatePullOut ( slice ) {

  // Выдвигаем сектор на шаг анимации
  currentPullOutDistance += pullOutFrameStep;

  // Если сектор выдвинут до нужного положения - заканчиваем анимацию
  if ( currentPullOutDistance >= maxPullOutDistance ) {
    clearInterval( animationId );
    return;
  }

  // Выводим кадр
  drawChart();
}


В функции просто добавляется значение переменной pullOutFrameStep к переменной currentPullOutDistance для выдвижения сектора еще на несколько пикселей, а затем вызывается функция drawChart() для вывода кадра. Также проверяется, если сектор уже выдвинут на максимальное расстояние (maxPullOutDistance). В случае подтверждения данного анимация останавливается вызовом функции clearInterval().
Шаг 11. Создаем функцию для возвращения сектора на место

Функция pushIn() вызывается функциями handleChartClick() и toggleSlice() тогда, когда нужно поставить любой выдвинутый сектор на место:

/**
* Задвигаем выдвинутые сектора на место.
*
* Сбрасывает переменные анимации и перерисовывает диаграмму.
* Также сбрасывает выделение строк в таблице.ы
*/

function pushIn() {
  currentPullOutSlice = -1;
  currentPullOutDistance = 0;
  clearInterval( animationId );
  drawChart();
  $('#chartData td').removeClass('highlight');
}


Данная функция сбрасывает значения переменных currentPullOutSlice и currentPullOutDistance, очищает любую анимацию с помощью вызова clearInterval(), перерисовывает диаграмму в соответствии с новыми условиями и удаляет выделение в таблице данных.

Шаг 12. Пишем функцию рисования диаграммы

Теперь напишем функцию, которая будет реально рисовать диаграмму! Функция drawChart() в действительности достаточно прямолинейная, так как она перекладывает часть работы на фукнцию drawSlice():
/**
* Рисуем диаграмму.
*
* Проходит циклом по всем секторам и рисует их.
*/

function drawChart() {

  // Получаем контекст для рисования
  var context = canvas.getContext('2d');
      
  // Очищаем область рисования
  context.clearRect ( 0, 0, canvasWidth, canvasHeight );

  // Рисуем каждый сектор диаграммы, пропуская выдвинутый (если он есть)
  for ( var slice in chartData ) {
    if ( slice != currentPullOutSlice ) drawSlice( context, slice );
  }

  // Если есть выдвинутый сектор, рисуем его.
  // (мы рисуем выдвинутый сектор последним, таким образом его тень не будет перекрываться другими секторами.)
  if ( currentPullOutSlice != -1 ) drawSlice( context, currentPullOutSlice );
}


Рассмотрим функцию по шагам:

Получаем контекст для рисования.
Для того чтобы нарисовать что-нибудь в элементе canvas нужно сначала получить контекст рисования. Это объект, который имеет набор методов для рисования в элементе canvas. Для получения контекста вызывается canvas.getContext(), которому передается параметр '2d', индицирующий что нам нужен двумерный контекст. (3D еще не реализован)

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

Рисуем все, за исключением выдвигаемого сектора.
Теперь можно циклом пройтись по всем секторам в массиве chartData и нарисовать сектора с помощью метода drawSlice(), за исключением выдвигаемого сектора.

Рисуем выдвигаемый сектор.
В завершении рисуем выдвигаемый сектор. Проверяем есть ли перемещаемый сектор с помощью проверки значения переменной currentPullOutSlice. если да, то вызываем функцию drawSlice() опять, передавая ей индекс выдвигаемого сектора.

Выдвигаемый сектор рисуется последним, потому что у него есть тень. Если рисовать все сектора в порядке следования в таблице, то сектора, которые будут выводиться после перемещаемого , могут перекрывать нарисованную тень.

Шаг 13. Cтроим функцию, которая рисует каждый сектор в диаграмме

Теперь делаем функцию, которая является сердцем скрипта. drawSlice() получает контекст рисования и индекс сектора, который будет выводиться:

/**
* Рисуем отдельный сектор в диаграмме.
*
* @param Context Контекст области рисования
* @param Number Индекс сектора
*/

function drawSlice ( context, slice ) {

  // Вычисляем выверенные начальный и конечный углы для сектора
  var startAngle = chartData[slice]['startAngle']  + chartStartAngle;
  var endAngle = chartData[slice]['endAngle']  + chartStartAngle;
    
  if ( slice == currentPullOutSlice ) {

    // Сектор выдвигается (или уже выдвинут).
    // Смещаем его от центра диаграммы, рисуем текстовую метку,
    // и добавляем тень.

    var midAngle = (startAngle + endAngle) / 2;
    var actualPullOutDistance = currentPullOutDistance * easeOut( currentPullOutDistance/maxPullOutDistance, .8 );
    startX = centreX + Math.cos(midAngle) * actualPullOutDistance;
    startY = centreY + Math.sin(midAngle) * actualPullOutDistance;
    context.fillStyle = 'rgb(' + chartColours[slice].join(',') + ')';
    context.textAlign = "center";
    context.font = pullOutLabelFont;
    context.fillText( chartData[slice]['label'], centreX + Math.cos(midAngle) * ( chartRadius + maxPullOutDistance + pullOutLabelPadding ), centreY + Math.sin(midAngle) * ( chartRadius + maxPullOutDistance + pullOutLabelPadding ) );
    context.font = pullOutValueFont;
    context.fillText( pullOutValuePrefix + chartData[slice]['value'] + " (" + ( parseInt( chartData[slice]['value'] / totalValue * 100 + .5 ) ) +  "%)", centreX + Math.cos(midAngle) * ( chartRadius + maxPullOutDistance + pullOutLabelPadding ), centreY + Math.sin(midAngle) * ( chartRadius + maxPullOutDistance + pullOutLabelPadding ) + 20 );
    context.shadowOffsetX = pullOutShadowOffsetX;
    context.shadowOffsetY = pullOutShadowOffsetY;
    context.shadowBlur = pullOutShadowBlur;

  } else {

    // Данный сектор не выдвинут, рисуем его от центра диаграммы
    startX = centreX;
    startY = centreY;
  }

  // Устанавливаем градиент для заполнения сектора
  var sliceGradient = context.createLinearGradient( 0, 0, canvasWidth*.75, canvasHeight*.75 );
  sliceGradient.addColorStop( 0, sliceGradientColour );
  sliceGradient.addColorStop( 1, 'rgb(' + chartColours[slice].join(',') + ')' );

  // Рисуем сектор
  context.beginPath();
  context.moveTo( startX, startY );
  context.arc( startX, startY, chartRadius, startAngle, endAngle, false );
  context.lineTo( startX, startY );
  context.closePath();
  context.fillStyle = sliceGradient;
  context.shadowColor = ( slice == currentPullOutSlice ) ? pullOutShadowColour : "rgba( 0, 0, 0, 0 )";
  context.fill();
  context.shadowColor = "rgba( 0, 0, 0, 0 )";

  // Задаем соответствующий стиль границы сектора
  if ( slice == currentPullOutSlice ) {
    context.lineWidth = pullOutBorderWidth;
    context.strokeStyle = pullOutBorderStyle;
  } else {
    context.lineWidth = sliceBorderWidth;
    context.strokeStyle = sliceBorderStyle;
  }

  // Рисуем границу сектора
  context.stroke();
}


Вот какие операции выполняет функция:

Вычисляем выверенные углы сектора.
Помниет, что отсчет угла сектора начинается с 0 радиан (3 часа), но мы хотим начинать отсчет с 12 часов. То есть нужно вычесть четверть оборота из угла сектора перед тем, как использовать его в рисовании. Операция выполняется с помощью переменной chartStartAngle, результат хранится в переменных startAngle и endAngle.

Для выдвигаемого сектора устанавливается смещение от центра диаграммы.
Следующий блок кода проверяет, выводим ли мы выдвигаемый сектор. Если да, то нужно установить смещение секктора относительно центра. Для этого мы вычисляем середину сектора (половина между начальным и конечным угломe), и получаем количество пикселей, на которое надо сдвинуть сектор с помощью умножения значения перменной currentPullOutDistance на результат функции easeOut(). Затем мы можем использовать простую тригонометрию (косинус и синус) для вычисления новой стартовой точки для сектора.

Для выдвигаемого сектора рисуем метку и устанавливаем тень.
После вычисления стартовой точки выдвигаемого сектора рисуем текстовую метку, которая состоит из метки из таблицы (например, "СуперШтука"), значения (например, "$1862.12") и процентного вырадения доли от общего количества. context.fillText() выводит текст после точкис координатами X и Y. Данные координаты вычисляются с использванием синуса и косинуса, прибавлением отступа (pullOutLabelPadding) от центра диаграммы, который предоставлет достаточно места для вывода метки.
Для обычного сектора рисование ведется от центра диаграммыы.
Если сектор не выдвигается, то стартовая точка - центр диаграммы.

Установка градиента.
Градиент добавляет немного шарма диаграмме. Мы используем метод context.createLinearGradient() для создания линейного градиента. Затем мы вызываем два раза addColorStop() чтобы добавить установить граничные цвета градиента ("#ddd" или светло серый и цвет сектора).

Рисуем сектор.
Для рисования вызываем beginPath(), который открывает контур. Затем перемещаем точку вывода с помощью метода moveTo()и рисуем дугу с помощью метода arc(). Данная функция рисует прямую линию от текущей точки под начальным углом длиной заданного радиуса, затем рисует дугу до точки, заданной конечным углом. Мы завершаем сегмент с помощью метода lineTo(), которому указывает в качестве конечной точки стартовую позицию сектора. Закрываем контур с помощью closePath(). Затем задаем градиент в качестве заполнения, добавляем тень для выдвигаемого сектора и вызываем метод fill() для заполнения сектора.

Последний аргумент функции arc() — false — сообщает функции о том, что дугу нужно рисовать против часовой стрелки.

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

Рисуем границу сектора.
В завершении рисуем тонкую светлую границу вокруг обычного сектора и более толстую темную границу вокруг выдвигаемого сектора. Ширину и цвет границы получаем из переменных, которые созданы в функции init(). Устанавливает значения свойств context.lineWidth и context.strokeStyle, а затем рисуем границу с помощью метода context.stroke().
Шаг 14. Создаем сглаживающую функцию

Последняя функция называется сглаживающей. Она вызывается функцией drawSlice(), ее назначение - затормозить анимацию к концу ее проведения:

/**
   * Вспомогательная функция вычисления плавности перехода
   *
   * Выглядит странно, но работает.
   *
   * @param Number Отношение текущей пройденной дистанции к максимальному расстоянию
   * @param Number Степень (чем выше число, тем плавнее переход)
   * @return Number Новое отношение
   */

  function easeOut( ratio, power ) {
    return ( Math.pow ( 1 - ratio, power ) + 1 );
  }

};


Код функции выглядит неким шаманством, но он работает. За счет манипулирования отношением между пройденным расстоянием и общей длиной пути, получается, что в начале сектор смещается на больший шаг, чем в конце своего перемещения.

Готово!

Другие новости по теме: