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

Виджет формы на данный момент выгдялит так:

На странице ноды таблица с данными выводится в таком виде:

Самое время воспользоваться jQuery и добавить динамики в наше составное поле.
Drupal использует jQuery и мы выбрали jQuery-плагин, который предназначен для создания диаграмм — Flot. Он создает графики на основании наборов данных «на лету» на стороне клиента.
Именно эта библиотека очень проста в использовании, хорошо выглядит и предоставляем много интересных штучек — например, зум или слежение за курсором.
Плагин работает в Internet Explorer 6/7/8, Firefox 2.x+, Safari 3.0+, Opera 9.5+ и Konqueror 4.x+ с HTML-тегом canvas (для IE используется эмуляция на JavaScript — excanvas).
Нам нужно показывать графики в 2х разных местах — на форме при создании или изменении ноды и на странице показа ноды. На странице показа ноды мы также использовали динамическое создание диаграмм, чтобы, во-первых — делать это на стороне клиента, а не нагружать сервер, а, во-вторых, — не тратить лишнего времени на разработку и тестирование системы кеширования изображений и повторно использовать код, который уже работает.
Сначала подключим код нашей библиотеки. Делаем мы это в hook_nodeapi(), а не в hook_init(), потому что нам нужно подгружать код только для страницы ноды или формы ноды, а hook_init() выполняется для каждой страницы. В случае произвольной формы нужно было бы использовать hook_init().
/** * Реализация hook_nodeapi(). */ function financial_table_nodeapi(&$node, $op, $a3 = NULL, $a4 = NULL) { if ($op == 'prepare' || $op == 'view' || $op == 'validate') { //Подключаем библиотеку excanvas для тупого ИЕ: financial_table_flot_add_js(); //Выполняется просмотр ноды или добавление/редактирование ноды. //Добавляем загрузку сжатой версии плагина Flot: drupal_add_js('sites/all/libraries/flot/jquery.flot.min.js'); //Подключаем стили: drupal_add_css(drupal_get_path('module', 'financial_table') . '/financial_table.css'); } //Добавляем загрузку нашего JS-кода, который и строит график: if ($op == 'view') { drupal_add_js(drupal_get_path('module', 'financial_table') . '/financial_table_view.js'); } elseif ($op == 'prepare' || $op == 'validate') { drupal_add_js(drupal_get_path('module', 'financial_table') . '/financial_table_edit.js'); } } /** * Вспомогательная функция для добавления excanvas для IE */ function financial_table_flot_add_js() { static $added; if ($added !== true) { $path = 'sites/all/libraries/flot'; // Different versions of flot have used different packing methods. Attempt to support both. $excanvas = file_exists("{$path}/excanvas.min.js") ? "{$path}/excanvas.min.js" : "{$path}/excanvas.pack.js"; drupal_set_html_head('<!--[if IE]><script language="javascript" type="text/javascript" src="'. base_path() . $excanvas . '"></script><![endif]-->'); $added = true; } }
Для работы нашего виджета мы в первой части статьи уже создали в файле template.php текущей темы функцию THEME_NAME_content_multiple_values($element). Теперь её нужно изменить.
Добавляем специальный класс к нашему элементу. Это поможет с помощью jQuery быстро найти все поля на странице и добавить к ним отрисовку графиков.
//Add a class for financial table fields. if ($field['type'] == 'financial_table') { $financial_table_class = ' financial-table-field'; }
Добавляем место для нашей диаграммы:
//Add a placeholder for Flot diagram. if ($field['type'] == 'financial_table') { $output .= '<div id="' . $table_id . '_placeholder" class="financal_table_placeholder" style="width:840px;height:350px;"></div>'; }
Со всеми изменениями функция выглядит так:
/** * Overriding of content module default rendering for multiple values * Used for node creation form. * Combine multiple values into a table with drag-n-drop reordering. */ function THEME_NAME_content_multiple_values($element) { $field_name = $element['#field_name']; $field = content_fields($field_name); $output = ''; // here is the definition of fields don't need to be reordered manually $fields_no_manual_reordering = array('field_employment', 'field_education'); $noReordering = in_array($field_name, $fields_no_manual_reordering); if ($field['multiple'] >= 1) { $table_id = $element['#field_name'] .'_values'; $order_class = $element['#field_name'] .'-delta-order'; $required = !empty($element['#required']) ? '<span class="form-required" title="'. t('This field is required.') .'">*</span>' : ''; if ($field['type'] == 'financial_table') { if(function_exists('financial_table_get_widget_header')) { $header = financial_table_get_widget_header($field, $required); } } else { $header = array( array( 'data' => t('!title: !required', array('!title' => $element['#title'], '!required' => $required)), 'colspan' => 2 ), $noReordering ? null : t('Order'), ); } $rows = array(); // Sort items according to '_weight' (needed when the form comes back after // preview or failed validation) $items = array(); foreach (element_children($element) as $key) { if ($key !== $element['#field_name'] .'_add_more') { $items[] = &$element[$key]; } } usort($items, '_content_sort_items_value_helper'); // Add the items as table rows. foreach ($items as $key => $item) { $item['_weight']['#attributes']['class'] = $order_class; $delta_element = drupal_render($item['_weight']); $cells = array( array('data' => '', 'class' => 'content-multiple-drag'), drupal_render($item), $noReordering ? null : array('data' => $delta_element, 'class' => 'delta-order'), ); $rows[] = array( 'data' => $cells, 'class' => 'draggable', ); } //Add a class for financial table fields. if ($field['type'] == 'financial_table') { $financial_table_class = ' financial-table-field'; } $output .= theme('table', $header, $rows, array('id' => $table_id, 'class' => 'content-multiple-table' . $financial_table_class) ); $output .= $element['#description'] ? '<div class="description">'. $element['#description'] .'</div>' : ''; $output .= drupal_render($element[$element['#field_name'] .'_add_more']); if (!$noReordering) { drupal_add_tabledrag($table_id, 'order', 'sibling', $order_class); } } else { foreach (element_children($element) as $key) { $output .= drupal_render($element[$key]); } } if ($output) { //Add a placeholder for Flot diagram. if ($field['type'] == 'financial_table') { $output .= '<div id="' . $table_id . '_placeholder" class="financal_table_placeholder" style="width:840px;height:350px;"></div>'; } if (($field['type'] == 'financial_table' || $field['type'] == 'funding_history_table') && $field['display_settings ']['label']['format'] == 'above') { $output = '<label for="' . $table_id .'">' . $element['#title'] . ': </label>' . $output; } } return $output; }
Вы подготовили HTML-код для отрисовки графиков — добавили место и дополнительные стили. Теперь нужно передать данные из формы в библиотеку Flot, которая по ним нарисует диаграмму.
Теперь под таблицей для ввода данных есть место для графика, но нам нужно передавать данные библиотеке, чтобы отрисовать то, что пользователь ввел. Делаем мы это с помощью JS-кода, который размещаем в файле financial_table_edit.js. Раньше мы уже добавили подключение этого файла в hook_nodeapi().
Drupal.behaviors.financial_table = function(context) { //Рисуем график в первый раз с данными, которые уже есть на форме: $('.financial-table-field').each(function () { Drupal.financial_table.redrawPlot($(this).attr('id')); }); //Перерисовываем график, если пользователь изменил значения полей. //ССK-полей этого типа на странице может быть несколько и мы ищем их по классу, //а не по ID. Если что-то изменилось — функции отрисовки графика //передается ID плейсхолдера для отрисовки графика. $('.financial_table_year').change(function(){ Drupal.financial_table.redrawPlot($(this).parents().filter('.financial-table-field').attr('id')); }); $('.financial_table_revenues').change(function(){ Drupal.financial_table.redrawPlot($(this).parents().filter('.financial-table-field').attr('id')); }); $('.financial_table_expenditures').change(function(){ Drupal.financial_table.redrawPlot($(this).parents().filter('.financial-table-field').attr('id')); }); $('.financial_table_net').change(function(){ Drupal.financial_table.redrawPlot($(this).parents().filter('.financial-table-field').attr('id')); }); }; /** * Создаем собственную область имен функций. */ Drupal.financial_table = {} Drupal.financial_table.redrawPlot = function (placeholderID) { var Rows = $('#' + placeholderID + ' tbody tr'); var revenues = new Array(); var expenditures = new Array(); var net = new Array(); Rows.each(function () { var FieldCell = $(this).find('td').eq(1); var year = FieldCell.find('.financial_table_year').val(); //Отрисовываем как только указан год if (year>0) { revenues.push([year, FieldCell.find('.financial_table_revenues').val()]); expenditures.push([year, FieldCell.find('.financial_table_expenditures').val()]); net.push([year, FieldCell.find('.financial_table_net').val()]); } }); //Сортируем данные, чтобы показывать красивый график независимо от того, //как отсортированы данные пользователем. var placeholder = $('#' + placeholderID + '_placeholder'); // Рисуем! var plot = $.plot(placeholder, [ { data: revenues.sort(Drupal.financial_table.sort), label: "Revenues" }, { data: expenditures.sort(Drupal.financial_table.sort), label: "Expenditures" }, { data: net.sort(Drupal.financial_table.sort), label: "Net" } ], { xaxis: { tickDecimals: 0},//Ось лет — показываем только целые числа yaxis: { //Ось сумм — добавляем тысячные разделители и знак «$» впереди tickDecimals: 0, tickFormatter: function (nStr) { //Источник: http://www.mredkj.com/javascript/nfbasic.html nStr += ''; x = nStr.split('.'); x1 = x[0]; x2 = x.length > 1 ? '.' + x[1] : ''; var rgx = /(\d+)(\d{3})/; while (rgx.test(x1)) { x1 = x1.replace(rgx, '$1' + ',' + '$2'); } return "$" + x1 + x2; } }, //Amount series: { lines: { show: true }, points: { show: true } } }); // Добавляем подписи к осям placeholder.append('<div style="left:-60px;top:160px" class="axis-legend">Amount</div>'); placeholder.append('<div style="left:405px;bottom:-10px" class="axis-legend">Years</div>'); }; // Функция сортировки для правильной последовательности лет на графике Drupal.financial_table.sort = function (a, b) { return (a[0] - b[0]); }
Вот как выглядит теперь виджет:

Отлично! При изменении данных в ячейках — меняется диаграмма. Осталось вывести график на странице просмотра ноды.
В первой части мы создали в текущей теме файл content-field.tpl.php и добавили туда свой код. Пришло время разместить там график:
<?php if ($field['type'] == 'financial_table') { if ($field['rendered_table']) { ?> <div class="field field-type-<?php print $field_type_css ?> field-<?php print $field_name_css ?>"> <?php if ($label_display == 'above') : ?> <div class="field-label"><?php print t($label) ?>: </div> <?php endif;?> <div class="field-items"> <?php echo $field['rendered_table'] ?> <?php if ($field['type'] == 'financial_table'): ?> <div id="<?php print $field['field_name'] ?>_placeholder" class="financal_table_placeholder" style="width:640px;height:350px;"></div> <?php endif; ?> </div> </div> <?php } } else { ?> <?php if (!$field_empty) : ?> <div class="field field-type-<?php print $field_type_css ?> field-<?php print $field_name_css ?>"> <?php if ($label_display == 'above') : ?> <div class="field-label"><?php print t($label) ?>: </div> <?php endif;?> <div class="field-items"> <?php $count = 1; foreach ($items as $delta => $item) : if (!$item['empty']) : ?> <div class="field-item <?php print ($count % 2 ? 'odd' : 'even') ?>"> <?php if ($label_display == 'inline') { ?> <div class="field-label-inline<?php print($delta ? '' : '-first')?>"> <?php print t($label) ?>: </div> <?php } ?> <?php print $item['view'] ?> </div> <?php $count++; endif; endforeach; ?> </div> </div> <?php endif; }
Добавлено место для диаграммы. Его размеры должны быть сразу заданы, чтобы Plot смог правильно масштабировать элементы графика.
Подготовим данные для графика. Делаем мы это в функции THEME_NAME_preprocess_content_field(&$vars)) и передаем нашему JS-скрипту через Drupal.settings.
function green_evolution_preprocess_content_field (&$vars) { if ($vars['field']['type'] == 'financial_table') { if (!$vars['field_empty']) { $header = array(t('Year'), t('Revenues'), t('Expenditures'), t('Net')); $rows = array(); foreach ($vars['items'] as $delta => $item) { if (!$item['empty']) { if(isset($item['financial_table_year']) && isset($item['financial_table_revenues']) && isset($item['financial_table_expenditures']) && isset($item['financial_table_net'])) { $rows[] = array( $item['financial_table_year'], $item['financial_table_revenues'], $item['financial_table_expenditures'], $item['financial_table_net'], ); $js_revenues[$item['financial_table_year']] = array($item['financial_table_year'], $item['financial_table_revenues']); $js_expenditures[$item['financial_table_year']] = array($item['financial_table_year'], $item['financial_table_expenditures']); $js_net[$item['financial_table_year']] = array($item['financial_table_year'], $item['financial_table_net']); } } } if(count($rows) > 0) { $vars['field']['rendered_table'] = theme('table', $header, $rows, array('id' => 'field-' . $field_name_css, 'class' => $field['type'])); //Sort all data to have a nice diagram ksort($js_revenues); ksort($js_net); ksort($js_expenditures); //Удаляем ключи и готовим массив для использования в качестве объекта JS $plot_data = array( $vars['field']['field_name'] . '_placeholder' => array( 'revenues' => array_values($js_revenues), 'expenditures' => array_values($js_expenditures), 'net' => array_values($js_net), ), ); //Передаем данные для использования на странице через Drupal.settings drupal_add_js($plot_data, 'setting'); } } } }
Осталось на основании данных, которые уже переданы в JS построить график, на месте, которое мы определили раньше. Для этого на странице просмотра ноды у нас подгружается скрипт financial_table_view.js:
/** * @file financial_table_view.js **/ Drupal.behaviors.financial_table = function(context) { //Find all financal_table placeholders $('.financal_table_placeholder').each(function () { var placeholderID = $(this).attr('id'); var placeholder = $("#" + placeholderID); // plot it var plotData = eval('Drupal.settings.' + placeholderID); var plot = $.plot(placeholder, [ { data: eval('Drupal.settings.' + placeholderID + '.revenues'), label: "Revenues" }, { data: eval('Drupal.settings.' + placeholderID + '.expenditures'), label: "Expenditures" }, { data: eval('Drupal.settings.' + placeholderID +'.net'), label: "Net" }, ], { yaxis: { tickDecimals: 0, tickFormatter: function (nStr) { //Source: http://www.mredkj.com/javascript/nfbasic.html nStr += ''; x = nStr.split('.'); x1 = x[0]; x2 = x.length > 1 ? '.' + x[1] : ''; var rgx = /(\d+)(\d{3})/; while (rgx.test(x1)) { x1 = x1.replace(rgx, '$1' + ',' + '$2'); } return "$" + x1 + x2; } }, xaxis: { tickDecimals: 0}, series: { lines: { show: true }, points: { show: true } } }); // add axis labels placeholder.append('<div style="left:-60px;top:160px" class="axis-legend">Amount</div>'); placeholder.append('<div style="left:300px;bottom:-10px" class="axis-legend">Years</div>'); }); }
Код подобен тому, что используется для виджета, за исключением того, что нам не нужно следить за изменением данных формы и перерисовывать график. Код ищет все места размещения гшрафика и для них ищет данные в Drupal.settings. Далее они используются, чтобы построить диаграмму:

Составное CCK-поле с динамическим графиком готово!
Хотите что-то добавить?