You are here

highcharttable.module in HighchartTable 7

highcharttable.module

Uses the Highcharts javascript library and HighchartTable jQuery add-on to decorate any HTML table on your site with a graphical visualisation as a colourful chart. Does NOT require Views.

File

highcharttable.module
View source
<?php

/**
 * @file
 * highcharttable.module
 *
 * Uses the Highcharts javascript library and HighchartTable jQuery add-on to
 * decorate any HTML table on your site with a graphical visualisation as a
 * colourful chart.
 * Does NOT require Views.
 */
define('HIGHCHARTTABLE_DEFAULT_TABLE_SELECTOR', 'table');
define('HIGHCHARTTABLE_CONTEXTUAL_LINKS_EXCLUDE_PAGES', '
admin/*
~admin/reports/access-denied
~admin/reports/page*
~admin/reports/referrers
~admin/reports/search
~admin/reports/visitors');

/**
 * Implements hook_preprocess_page().
 *
 * Many hooks qualify for what we do here...
 */
function highcharttable_preprocess_page($variables) {
  $module_path = drupal_get_path('module', 'highcharttable');

  // Add some Javascript, with a group and weight such that it will run before
  // modules/contextual/contextual.js
  // We need this on all pages because we do not know a-priori whether the
  // page to be loaded will have HTML tables on it or not -- that is exactly
  // what this Javascript figures out!
  $js = $module_path . '/js/highcharttable.js';
  drupal_add_js($js, array(
    'group' => JS_LIBRARY,
    'weight' => -1,
  ));
  $global_settings = variable_get('highcharttable_global_settings', array());
  $path = current_path();
  if (user_access('configure chart decorations')) {
    $exclude_pages = isset($global_settings['contextual-links-exclude-pages']) ? $global_settings['contextual-links-exclude-pages'] : HIGHCHARTTABLE_CONTEXTUAL_LINKS_EXCLUDE_PAGES;
    if (highcharttable_match_path($path, $exclude_pages)) {
      $global_settings['contextual-links'] = 'off';
    }
  }
  else {
    $global_settings['contextual-links'] = 'off';
  }
  if ($global_settings['contextual-links'] != 'off') {
    drupal_add_css($module_path . '/css/highcharttable.css');
    if ($global_settings['contextual-links'] == '') {

      // Default core-style contexutual links behaviour.
      // These lines will do nothing, if already included by core Contextual.
      $contextual_path = drupal_get_path('module', 'contextual');
      drupal_add_css($contextual_path . '/contextual.css');
      drupal_add_js($contextual_path . '/contextual.js');
    }
  }

  // First, pass the global settings to the js.
  drupal_add_js(array(
    'highcharttable' => array(
      'global' => $global_settings,
    ),
  ), array(
    'type' => 'setting',
  ));

  // ... now pass the decorations too.
  $path_alias = drupal_get_path_alias($path);
  $decoration_settings = variable_get('highcharttable_decorations', array());
  foreach ($decoration_settings as $decoration) {
    if (!empty($decoration['pages-and-selector'])) {
      $include_pages = $decoration['pages-and-selector']['include-pages'];
      if (highcharttable_match_path($path, $include_pages) || highcharttable_match_path($path_alias, $include_pages)) {
        $settings = highcharttable_get_js_settings($global_settings, $decoration);
        drupal_add_js(array(
          'highcharttable' => $settings,
        ), array(
          'type' => 'setting',
        ));
        if (empty($is_loaded)) {
          $variant = empty($global_settings['use-patched-library']) ? NULL : 'patched';
          highcharttable_load_libs($variant);
          $is_loaded = TRUE;
        }
      }
    }
  }
}

/**
 * Set up an array of configurations for the DataTables JS call.
 *
 * @param array $decoration
 *   An array of HighchartTable jQuery settings, indexed by the table selector
 */
function highcharttable_get_js_settings($global_settings, $decoration) {
  $table_selector = empty($decoration['pages-and-selector']['table-selector']) ? HIGHCHARTTABLE_DEFAULT_TABLE_SELECTOR : $decoration['pages-and-selector']['table-selector'];
  $decoration_params = empty($decoration['decoration-params']) ? array() : $decoration['decoration-params'];
  $chart_type = isset($decoration_params['chart-type']) ? $decoration_params['chart-type'] : NULL;
  $settings = array(
    $table_selector => array(
      'animation' => empty($global_settings['animation']) ? NULL : $global_settings['animation'],
      'color-1' => empty($decoration_params['color-1']) ? NULL : $decoration_params['color-1'],
      'color-2' => empty($decoration_params['color-2']) ? NULL : $decoration_params['color-2'],
      'color-3' => empty($decoration_params['color-3']) ? NULL : $decoration_params['color-3'],
      'container-before' => 1,
      'datalabels-enabled' => !empty($decoration_params['labels']),
      'height' => empty($decoration_params['height']) ? NULL : (int) $decoration_params['height'],
      'hide-table' => !empty($decoration['pages-and-selector']['hide-table']),
      'inverted' => !empty($decoration_params['swap-axes']),
      'legend-disabled' => empty($decoration_params['legend']),
      'legend-layout' => empty($decoration_params['legend']) ? NULL : $decoration_params['legend'],
      'pie-show-in-legend' => $chart_type == 'pie' && !empty($decoration_params['legend']),
      'subtitle-text' => empty($decoration_params['subtitle']) ? NULL : filter_xss_admin($decoration_params['subtitle']),
      'suppress-invalid-series' => isset($decoration_params['suppress-invalid-series']) ? $decoration_params['suppress-invalid-series'] : 2,
      'type' => $chart_type,
      'xaxis' => empty($decoration_params['xaxis']) ? 0 : max(0, (int) $decoration_params['xaxis'] - 1),
      // Only doing formatter for primary yaxis, ignoring opposite yaxes.
      'yaxis-1-formatter-callback' => empty($decoration_params['formatter']) ? NULL : $decoration_params['formatter'],
    ),
  );
  return $settings;
}

/**
 * Include all required external javascript code.
 *
 * Highcharts JS and HighchartTable jQuery libraries.
 *
 * @param $variant, string
 *   Either NULL for the original version or 'patched' for the local version.
 */
function highcharttable_load_libs($variant) {

  // Equivalent to core's drupal_add_library('highcharttable', 'highcharts'),
  // this loads what is set up in the .libraries.info file, rather than core's
  // hook_library(). The format of the array returned is similar.
  $highcharts_lib = libraries_load('highcharts');
  if (!empty($highcharts_lib['error'])) {
    drupal_set_message($highcharts_lib['error message'], 'warning');
  }
  $highcharttable_lib = libraries_load('highcharttable', $variant);
  if (!empty($highcharttable_lib['error'])) {
    drupal_set_message($highcharttable_lib['error message'], 'warning');
  }
}

/**
 * Implements hook_menu().
 */
function highcharttable_menu() {

  // Put the administrative settings, on the Configuration page, under Content
  $items['admin/config/content/highcharttable'] = array(
    'title' => 'HighchartTable',
    'description' => 'Configure chart decorations and global settings.',
    'page callback' => 'drupal_get_form',
    'page arguments' => array(
      'highcharttable_admin_configure_form',
    ),
    'access arguments' => array(
      'configure chart decorations',
    ),
    'file' => 'highcharttable.admin.inc',
  );
  $items['highcharttable/contextual-link'] = array(
    'title' => 'HighchartTable contextual links',
    'description' => 'HighchartTable contextual link',
    'page callback' => 'highcharttable_contextual_link',
    'access arguments' => array(
      'configure chart decorations',
    ),
  );
  return $items;
}

/**
 * Menu callback for highcharttable/contextual-link
 *
 * @param string $operation ($arg0)
 *   The operation to perform, one of 'insert', 'delete'.
 *   'configure' is obsolete as this operation can be done via a direct link.
 * @param string $chart_type ($arg1)
 *   The chart type, one of 'column', 'line', 'spline', 'area', 'pie'.
 *   Defaults to 'column'.
 * @param string $arg2
 *   Optional argument, not used
 * @param string $arg3
 *   Optional argument, not used
 */
function highcharttable_contextual_link($operation = NULL, $chart_type = NULL, $arg2 = NULL, $arg3 = NULL) {
  $decorations = variable_get('highcharttable_decorations', array());
  $page = filter_input(INPUT_GET, 'destination');
  $url_options = array(
    'query' => array(
      'destination' => $page,
    ),
  );
  switch ($operation) {
    case 'insert':
      highcharttable_add_decoration($page, $chart_type, $decorations);
      break;
    case 'delete':
      highcharttable_remove_page_from_decorations($page, $decorations);
      break;
    default:

      // Not used. As there are no actions required for 'configure', link to
      // this page directly, rather than using this function.
      drupal_goto('admin/config/content/highcharttable', $url_options);
  }

  // Make sure to save with keys that are numeric, consecutive and start at 0.
  variable_set('highcharttable_decorations', array_values($decorations));

  // Having created/deleted the chart decoration we return where we came from.
  drupal_goto($page, $url_options);
}

/**
 * Add a new char decoration, using the supplied page path and chart type.
 *
 * @param string $page
 * @param string $chart_type, defaults to 'column'
 * @param array $decorations
 */
function highcharttable_add_decoration($page, $chart_type = NULL, &$decorations) {
  $decoration = array(
    'decoration-params' => array(
      'chart-type' => empty($chart_type) ? 'column' : $chart_type,
    ),
    'pages-and-selector' => array(
      'include-pages' => $page,
      'table-selector' => '',
    ),
  );
  $decorations[] = $decoration;
}

/**
 * Removes the supplied page path from all decorations that use it.
 *
 * If as a result of this the decoration no longer has any 'include-pages', then
 * the decoration is removed altogether.
 *
 * @param string $page
 * @param array $decorations
 */
function highcharttable_remove_page_from_decorations($page, &$decorations) {
  $url_options = array(
    'query' => array(
      'destination' => $page,
    ),
  );
  foreach ($decorations as $key => $decoration) {
    $include_pages = trim($decoration['pages-and-selector']['include-pages']);

    // Remove page path from 'include-pages', if found.
    $reduced = str_replace($page, '', $include_pages);
    if (strlen($reduced) < strlen($include_pages)) {
      $is_deleted = TRUE;
      if (empty($reduced)) {
        unset($decorations[$key]);
        drupal_set_message(t('Chart and chart decoration removed.'));
      }
      else {
        $decorations[$key]['pages-and-selector']['include-pages'] = $reduced;
        drupal_set_message(t('Chart removed. However, the underlying chart decoration was retained, as it appears to be used on other pages. Visit the <a href="@url_config">configuration page</a> to learn more.', array(
          '@url_config' => url('admin/config/content/highcharttable', $url_options),
        )));
      }
    }
  }
  if (empty($is_deleted)) {
    drupal_set_message(t('The chart was not removed. Most likely this was because this page is part of a wild-card specification shared with other charts. Visit the <a href="@url_config">configuration page</a> to learn more.', array(
      '@url_config' => url('admin/config/content/highcharttable', $url_options),
    )));
  }
}

/**
 * Match a path against one or more regex patterns.
 *
 * A drop-in replacement for drupal_match_path(), this also handles pattern
 * negation through the use of the twiddle/squiggle (~) character.
 *
 * Adapted from function context_condition_path::match() in the Context module.
 *
 * @param string $path
 *   The path string to be matched.
 * @param string $patterns
 *   String containing a sequence of patterns separated by \n, \r or \r\n.
 *   Each pattern will be trimmed of leading spaces.
 *   Any pattern that begins with ~ is interpreted as an exclusion and overrides
 *   any match amongst patterns.
 *   Example:
 *   With $patterns = "admin/*\n~admin/reports/visitors", this function will
 *   return TRUE for all "admin/*" paths, except "admin/reports/visitors".
 */
function highcharttable_match_path($path, $patterns) {
  $regexps =& drupal_static(__FUNCTION__, array());
  $patterns = preg_split('/[\\r\\n]+/', $patterns);
  $match = FALSE;
  $positives = $negatives = 0;
  foreach ($patterns as $pat) {
    $pattern = trim($pat);
    if (!empty($pattern)) {
      if ($negate = drupal_substr($pattern, 0, 1) === '~') {

        // Remove the tilde before executing a standard preg_match().
        $pattern = drupal_substr($pattern, 1);
        $negatives++;
      }
      else {
        $positives++;
      }
      if (!isset($regexps[$pattern])) {
        $regexps[$pattern] = '/^(' . preg_replace(array(
          '/(\\r\\n?|\\n)/',
          '/\\\\\\*/',
          '/(^|\\|)\\\\<front\\\\>($|\\|)/',
        ), array(
          '|',
          '.*',
          '\\1' . preg_quote(variable_get('site_frontpage', 'node'), '/') . '\\2',
        ), preg_quote($pattern, '/')) . ')$/';
      }
      if (preg_match($regexps[$pattern], $path)) {
        if ($negate) {
          return FALSE;
        }
        $match = TRUE;
      }
    }
  }

  // If there are *only* negative conditions and we got this far, we actually
  // have a match.
  if ($positives === 0 && $negatives > 0) {
    return TRUE;
  }
  return $match;
}

/**
 * Implements hook_permission().
 */
function highcharttable_permission() {
  return array(
    'configure chart decorations' => array(
      'title' => t('Add and configure chart decorations'),
    ),
  );
}

/**
 * Implements hook_help().
 */
function highcharttable_help($path, $arg) {
  switch ($path) {
    case 'admin/help#highcharttable':
      $t = t('Configuration instructions and tips are in this <a target="readme" href="@README">README</a> file.<br/>Known issues and solutions may be found on the <a taget="project" href="@highcharttable">HighchartTable</a> project page.', array(
        '@README' => url(drupal_get_path('module', 'highcharttable') . '/README.txt'),
        '@highcharttable' => url('http://drupal.org/project/highcharttable'),
      ));
      break;
    case 'admin/config/content/highcharttable':
      $t = t('A <strong>chart decoration</strong> consists of a set of chart options, selected below. Apart from the features you wish to include in each chart decoration, you specify the pages the chart(s) should appear on.');
      break;
  }
  return empty($t) ? '' : '<p>' . $t . '</p>';
}

/**
 * Implements hook_libraries_info_file_paths().
 *
 * Using the .libraries.info files instead of hook_libraries_info().
 */
function highcharttable_libraries_info_file_paths() {
  return array(
    drupal_get_path('module', 'highcharttable') . '/libraries',
  );
}

/**
 * Implements hook_libraries_info_alter().
 *
 * This is a dynamic appendix to the .libraries.info files.
 * Through the configuration page, the user may opt for a variant, used here.
 */
function highcharttable_libraries_info_alter(&$libraries) {
  if (!isset($libraries['highcharttable'])) {
    return;
  }

  // The d.o. packaging script unfortunately adds to the .libraries.info file
  // the version number of the module in the same way as it does for
  // highcharttables.info. This is bad news for us, as once set, the Libraries
  // module will not attempt to read it from the specified files. So unset.
  unset($libraries['highcharts']['version']);
  unset($libraries['highcharttable']['version']);
  $global_settings = variable_get('highcharttable_global_settings', array());
  $variant = empty($global_settings['use-patched-library']) ? NULL : 'patched';

  // Based on the variant, the version number is found in a different file.
  // Not only is the file where we obtain the version different, so is its path.
  // The default path is sites/all/libraries/highcharttable, the variant path
  // is the module path.
  if ($variant == 'patched') {
    $libraries['highcharttable']['library path'] = drupal_get_path('module', 'highcharttable');
  }
  $libraries['highcharttable']['version arguments']['file'] = $variant == 'patched' ? 'libraries/variants/highchartTable-patched.jquery.json' : 'highchartTable.jquery.json';
}

/**
 * Returns whether path matches any of the wildcards in decoration.
 *
 * @param string $path
 * @param array $decoration
 *
 * @return bool
 */
function _highcharttable_path_matches_decoration($path, $decoration) {
  return !empty($decoration['pages-and-selector']) && highcharttable_match_path($path, $decoration['pages-and-selector']['include-pages']);
}

Functions

Namesort descending Description
highcharttable_add_decoration Add a new char decoration, using the supplied page path and chart type.
highcharttable_contextual_link Menu callback for highcharttable/contextual-link
highcharttable_get_js_settings Set up an array of configurations for the DataTables JS call.
highcharttable_help Implements hook_help().
highcharttable_libraries_info_alter Implements hook_libraries_info_alter().
highcharttable_libraries_info_file_paths Implements hook_libraries_info_file_paths().
highcharttable_load_libs Include all required external javascript code.
highcharttable_match_path Match a path against one or more regex patterns.
highcharttable_menu Implements hook_menu().
highcharttable_permission Implements hook_permission().
highcharttable_preprocess_page Implements hook_preprocess_page().
highcharttable_remove_page_from_decorations Removes the supplied page path from all decorations that use it.
_highcharttable_path_matches_decoration Returns whether path matches any of the wildcards in decoration.

Constants