ShvetsGroup

 

Captions

RSS Следите за нашим блогом и будьте в курсе последних новостей.

 

Создание сложного поля CCK с диаграммой (ч2)

0 комментариев

Создание сложного поля CCK с диаграммой (ч2)

В первой части статьи мы создали составное CCK-поле, которое позволяет пользователю вводить данные в виде таблицы и также показывает эти данные на странице ноды в виде красивой таблицы.

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

Исходные требования

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

виджет формы составного CCK-поля

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

отображение таблицы составного 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('');

    $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 .= '
'; }

Со всеми изменениями функция выглядит так:


/**
 * 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']) ? '*' : '';

    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'] ? '
'. $element['#description'] .'
' : ''; $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 .= '
'; } if (($field['type'] == 'financial_table' || $field['type'] == 'funding_history_table') && $field['display_settings ']['label']['format'] == 'above') { $output = '' . $output; } } return $output; }

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

Добавление JS-кода, рисующего график на форме

Теперь под таблицей для ввода данных есть место для графика, но нам нужно передавать данные библиотеке, чтобы отрисовать то, что пользователь ввел. Делаем мы это с помощью 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('
Amount
'); placeholder.append('
Years
'); }; // Функция сортировки для правильной последовательности лет на графике Drupal.financial_table.sort = function (a, b) { return (a[0] - b[0]); }

Вот как выглядит теперь виджет:

составное CCK-поле и динамическая диаграмма

Отлично! При изменении данных в ячейках — меняется диаграмма. Осталось вывести график на странице просмотра ноды.

Динамическая диаграмма на странице просмотра ноды

В первой части мы создали в текущей теме файл content-field.tpl.php и добавили туда свой код. Пришло время разместить там график:


$item) : if (!$item['empty']) : ?>

Добавлено место для диаграммы. Его размеры должны быть сразу заданы, чтобы 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-кода для рисования графика на странице ноды

Осталось на основании данных, которые уже переданы в 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('
Amount
'); placeholder.append('
Years
'); }); }

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

составное CCK-поле с диаграммой на странице ноды

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

Библиология

Обзоры библиотек

JS-библиотеки для создания SVG изображений

SVG

Построение графиков на основании HTML-таблиц

Хотите что-то добавить?