Статьи

Fun With Canvas: Создание плагина для гистограммы, часть 1

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

Сегодня мы собираемся создать плагин для построения графиков. Не обычный плагин, заметьте. Мы покажем некоторую любовь jQuery к элементу canvas, чтобы создать очень надежный плагин.

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

Нужен пример, прежде чем мы начнем? Ну вот!


Различные графики, созданные с использованием различных настроек для нашего плагина

Доволен? Заинтересованы еще? Давайте начнем.


Наш плагин должен выполнить некоторые основные вещи, в то время как другие не делают. Позвольте мне объяснить:

  • Как обычно, мы собираемся использовать только элемент canvas и JavaScript. Нет изображений любого вида, нет сломанных методов CSS, нет предварительной визуализации. Простой старый (или новый?) Элемент canvas вместе с некоторым jQuery для облегчения нашей рабочей нагрузки.
  • Что касается источника данных, мы собираемся извлечь все данные непосредственно из стандартной таблицы. Нет массивов для передачи на плагин при запуске. Таким образом, пользователь может просто поместить все данные в таблицу, а затем вызвать наш плагин. Плюс, это намного более доступно.
  • Нет специальной разметки для таблицы, выступающей в качестве источника данных, и определенно нет специальных имен классов для ячеек данных. Мы собираемся использовать только идентификатор таблицы и извлекать все наши данные оттуда.
  • Отсутствие надёжного наложения текста для рендеринга надписей и т.п. на графике. Это не только очень утомительно, но отображаемый текст не является частью графика, когда он сохраняется. Мы будем использовать fillText и strokeText, как определено спецификациями WHATWG.

Поскольку мы углубляемся в мир передовых, все еще не полностью определенных технологий, у нас есть некоторые зависимости. Для работы элемента canvas достаточно современных браузеров. Но поскольку мы используем новый API для рендеринга текста, нам нужны более новые сборки. Браузеры, использующие движок Webkit r433xx и выше или движок Gecko 1.9.1 и выше, должны быть отличными платформами для плагина. Я рекомендую захватить ночные сборки Chromium или Firefox.


Я хотел бы отметить, что наш плагин предназначен исключительно для учебных целей. Этот плагин никоим образом не предназначен для замены других полноценных графических плагинов, таких как Flot, Plotr и тому подобное. Кроме того, код будет максимально подробным. Вы могли бы написать гораздо более эффективный код, но ради обучения все будет максимально простым. Не стесняйтесь рефакторинг в пользу эффективности в вашем производственном коде.


01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
<!DOCTYPE html>
<html lang=»en-GB»>
<head>
<title>OMG WTF HAX</title>
</head>
 
<body>
 
<table width=»200″ border=»0″ id=»data»>
 
 <tr>
    <th>Year</th>
    <th>Sales</th>
 </tr>
 
 <tr>
    <td>2009</td>
    <td>130</td>
 </tr>
 
 <tr>
    <td>2008</td>
    <td>200</td>
 </tr>
 
 <tr>
    <td>2007</td>
    <td>145</td>
 </tr>
 
 <tr>
    <td>2006</td>
    <td>140</td>
 </tr>
 
 <tr>
    <td>2005</td>
    <td>210</td>
 </tr>
 
 <tr>
    <td>2004</td>
    <td>250</td>
 </tr>
 
 <tr>
    <td>2003</td>
    <td>170</td>
 </tr>
 
 <tr>
    <td>2002</td>
    <td>215</td>
 </tr>
 
 <tr>
    <td>2001</td>
    <td>115</td>
 </tr>
 
 <tr>
    <td>2000</td>
    <td>135</td>
 </tr>
 <tr>
    <td>1999</td>
    <td>110</td>
 </tr>
 
 <tr>
    <td>1998</td>
    <td>180</td>
 </tr>
 
 <tr>
    <td>1997</td>
    <td>105</td>
 </tr>
 
</table>
 
<canvas id=»graph» width=»550″ height=»220″></canvas>
 
<script type=»text/javascript» src=»jquery.js»></script>
<script type=»text/javascript» src=»mocha.js»></script>
 
</body>
</html>

Ничего особенного в разметке. Я сделаю краткий обзор в любом случае.

  • Мы начнем с включения необходимого типа документа. Поскольку мы используем элемент canvas, мы используем соответствующий элемент для HTML 5.
  • Затем определяется таблица источника данных. Заметьте, что никакой специальной разметки не описывается или новые классы определяются и назначаются внутри ее членов.
  • Элемент canvas определяется, а затем ему присваивается идентификатор для последующей ссылки на него. Этот конкретный элемент canvas будет только для автономной версии. В версии плагина элемент canvas и его атрибуты будут динамически внедряться в DOM, а затем обрабатываться по мере необходимости. Для прогрессивного улучшения этот способ работает намного лучше.
  • Наконец, мы включаем библиотеку jQuery и наш собственный скрипт. Как Джеффри упоминал снова и снова, включение сценариев в конец документа всегда хорошая идея.

Прежде чем мы начнем Javascript, позвольте мне объяснить систему координат холста. Верхний левый угол действует как источник, то есть (0, 0). Точки затем измеряются относительно начала координат с увеличением x вдоль правой и увеличением y вдоль левой. Для математически склонных мы эффективно работаем в 4-м квадранте, за исключением того, что мы берем абсолютное значение у вместо его отрицательного значения. Если вы работали с графикой на других языках, вы должны быть здесь дома.


Система координат холста

Процедура рендеринга прямоугольника Canvas будет широко использоваться в статье для визуализации баров, сетки и некоторых других элементов. Имея это в виду, давайте кратко рассмотрим эти процедуры.

Из трех доступных подпрограмм мы будем использовать методы fillRect и strokeRect . Метод fillRect фактически заполняет визуализированный прямоугольник, в то время как метод strokeRect только обводит прямоугольники. Кроме этого, оба метода принимают одинаковые параметры.

  • x — Координата x точки, с которой начинается рисование.
  • y — координата y относительно начала координат.
  • ширина — определяет ширину прямоугольника, который будет нарисован.
  • высота — определяет высоту прямоугольника.

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


01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
var
    barSpacing = 20,
    barWidth = 20,
    cvHeight = 220,
    numYlabels = 8,
    xOffset = 20,
    gWidth=550,
    gHeight=200;
 
var maxVal,
    gValues = [],
    xLabels = [],
    yLabels = [];
       
var cv, ctx;
  • xLabels — Массив, который содержит значение меток оси X.
  • yLabels — То же, что и выше, за исключением того, что он содержит значения меток оси Y.
  • gValues — Массив, который содержит все данные графика, которые мы извлекаем из источника данных.
  • cv — переменная, указывающая на элемент canvas.
  • ctx — переменная для ссылки на контекст элемента canvas.

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

  • barSpacing — определяет расстояние между отдельными барами.
  • barWidth — определяет ширину каждого отдельного бара.
  • cvHeight — определяет высоту элемента canvas. Жестко закодировано, так как мы создали элемент canvas заранее. Версия плагина зависит от этой функциональности.
  • numYlabels — определяет количество меток, которые будут нарисованы на оси Y.
  • xOffset — Определяет пространство между началом элемента canvas и фактическим графиком. Это пространство используется для рисования меток оси Y.
  • gWidth, gHeight — жестко запрограммированные значения, содержащие размер фактического пространства рендеринга самого графика.

Как каждая переменная контролирует внешний вид графика

С помощью мощного механизма выбора jQuery нам становится очень легко получать необходимые данные. Здесь у нас есть несколько способов доступа к необходимым элементам. Позвольте мне объяснить несколько ниже:

1
2
3
$(«tr»).children(«td:odd»).each(function(){
//code here
});

Самый простой способ получить доступ к необходимым строкам. Ищет элемент tr, а затем обращается к любому другому элементу td . Неудачно, когда у вас есть более одной таблицы на вашей странице.

1
2
3
$(«#data»).find(«td:odd»).each(function(){
//code here
});

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

1
2
3
$(«#data tr td:odd»).each(function(){
//code here
});

То же, что и выше, за исключением того, что мы просто используем синтаксис селектора стиля CSS.

1
2
3
$(«#data tr td:nth-child(2)»).each(function(){
//code here
});

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

Финальная версия выглядит так:

01
02
03
04
05
06
07
08
09
10
11
12
function grabValues ()
     {
        // Access the required table cell, extract and add its value to the values array.
         $(«#data tr td:nth-child(2)»).each(function(){
         gValues.push($(this).text());
         });
      
         // Access the required table cell, extract and add its value to the xLabels array.
         $(«#data tr td:nth-child(1)»).each(function(){
        xLabels.push($(this).text());
         });
     }

Здесь ничего сложного. Мы используем фрагмент кода, упомянутый выше, чтобы добавить значение ячейки таблицы в массив gValues . Далее мы делаем то же самое, за исключением того, что мы обращаемся к первой ячейке таблицы, чтобы извлечь необходимую метку для оси x. Мы инкапсулировали логику извлечения данных в ее собственную функцию для повторного использования кода и читабельности.


01
02
03
04
05
06
07
08
09
10
11
12
function initCanvas ()
     {
        // Try to access the canvas element and throw an error if it isn’t available
        cv = $(«#graph»).get(0);
        if (!cv)
        { return;
      
        // Try to get a 2D context for the canvas and throw an error if unable to
        ctx = cv.getContext(‘2d’);
        if (!ctx)
        { return;
     }

Обычная инициализация холста. Сначала мы пытаемся получить доступ к самому элементу canvas. Мы выдаем ошибку, если не можем. Затем мы пытаемся получить ссылку на 2-й контекст рендеринга с помощью метода getContext и выдаем ошибку, если мы не можем этого сделать.


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

01
02
03
04
05
06
07
08
09
10
11
12
13
14
function maxValues (arr)
     {
        maxVal=0;
         
        for(i=0; i<arr.length; i++)
        {
         if (maxVal<parseInt(arr[i]))
         {
         maxVal=parseInt(arr[i]);
         }
        }
         
       maxVal*= 1.1;
     }

Небольшая функция, которая перебирает переданный массив и обновляет переменную maxVal . Обратите внимание, что мы увеличиваем максимальное значение на 10% для специальных целей. Если максимальное значение оставить таким, как оно есть, то полоса, представляющая самое верхнее значение, коснется края элемента canvas, который нам не нужен. С учетом этого выдается 10% прирост.

1
2
3
4
function scale (param)
      {
       return Math.round((param/maxVal)*gHeight);
      }

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

1
2
3
4
function x (param)
      {
       return (param*barWidth)+((param+1)*barSpacing)+xOffset;
      }

Возвращает ординату x в fillRect, чтобы помочь нам в позиционировании каждого отдельного бара. Я объясню это немного подробнее, когда это будет использовано.

1
2
3
4
function y (param)
      {
       return gHeight — scale (param) ;
      }

Возвращает ординату y в метод fillRect, чтобы помочь нам в позиционировании каждого отдельного бара. Больше объяснений чуть позже.

1
2
3
4
function width ()
      {
       return barWidth;
      }

Возвращает ширину каждого отдельного бара.

1
2
3
4
function height (param)
      {
       return scale(param);
      }

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


01
02
03
04
05
06
07
08
09
10
11
function drawXlabels ()
      {
         ctx.save();
         ctx.font = «10px ‘arial'»;
         ctx.fillStyle = «#000»;
         for(index=0; index<gValues.length; index++)
         {
         ctx.fillText(xLabels[index], x(index), gHeight+17);
         }
         ctx.restore();
      }

Простая функция для визуализации меток оси X. Сначала мы сохраняем текущее состояние холста, включая все настройки рендеринга, чтобы все, что мы делаем внутри функций, никогда не просачивалось. Затем мы устанавливаем размер и шрифт надписей. Далее мы перебираем массив xLabels и каждый раз вызываем метод fillText для визуализации метки. Мы используем функцию x, чтобы помочь нам в позиционировании меток.


01
02
03
04
05
06
07
08
09
10
11
12
function drawYlabels()
      {
         ctx.save();
         for(index=0; index<numYlabels; index++)
          {
           yLabels.push(Math.round(maxVal/numYlabels*(index+1)));
           ctx.fillStyle = «#000»;
           ctx.fillText(yLabels[index], xOffset, y(yLabels[index])+10);
           }
           ctx.fillText(«0», xOffset, gHeight+7);
           ctx.restore();
      }

Чуть более многословная функция. Сначала мы сохраняем текущее состояние холста, а затем продолжаем. Далее мы делим значение maxVal на n элементов, где переменная numYlabels определяет n. Эти значения затем добавляются в массив yLabels . Теперь, как показано выше, метод fillText вызывается для рисования отдельных меток с помощью функции y, помогающей нам в позиционировании каждой отдельной метки.

Мы рисуем ноль в нижней части холста, чтобы закончить рисование меток Y.


01
02
03
04
05
06
07
08
09
10
function drawGraph ()
     {
        for(index=0; index<gValues.length; index++)
          {
            ctx.save();
            ctx.fillStyle = «#B7B7B7»;
            ctx.fillRect( x(index), y(gValues[index]), width(), height(gValues[index]));
            ctx.restore();
          }
     }

Функция, которая рисует фактические бары в гистограмме. Итерация по массиву gValues и рендеринг каждого отдельного бара. Мы используем метод fillRect для рисования баров. Как объяснено выше, метод принимает четыре параметра, каждый из которых учитывается нашими функциями полезности. Текущий индекс цикла передается нашим функциям в качестве параметров вместе с фактическим значением, хранящимся в массиве, по мере необходимости.

Функция x возвращает координату x столбца. Каждый раз он увеличивается на величину суммы переменных barWidth и barSpacing .

Функция y вычисляет разницу между высотой элемента canvas и нормализованных данных и возвращает ее. Я знаю, что это звучит немного перевернуто, но это связано с тем, что значения y на сетке холста увеличиваются при движении вниз, в то время как на нашем графике значения y увеличиваются при движении вверх. Таким образом, нам нужно проделать небольшую работу, чтобы заставить ее функционировать так, как мы хотим.

Функция width возвращает ширину отдельных стержней.

Функция высоты просто возвращает нормализованное значение, которое будет использоваться в качестве высоты отображаемой панели.


В этой первой части мы реализовали базовую логику нашего плагина как автономную версию с простыми взглядами и функциями. Мы рассмотрели систему координат холста, методы рендеринга прямоугольника, изящное извлечение данных с использованием врожденной удивительности jQuery [я уже упоминал, насколько мне нравится jQuery?], Посмотрели на то, как рисуются метки, и наконец взглянули на логику рендеринга сам график.

В конце этой статьи вывод должен выглядеть так.


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

  • Реорганизовать наш код, сделав его полноценным плагином jQuery.
  • Добавьте немного глазных конфет.
  • Включите некоторые изящные маленькие особенности.

Вопросов? Критицизмы? Похвалы? Не стесняйтесь нажимать на комментарии. Спасибо за чтение и, когда вы будете готовы, перейдите ко второй части!