You are here

d3.module in d3.js 7

D3 module file for creating visualizations with d3.js.

Sponsored by http://www.freeflowdigital.com

File

d3.module
View source
<?php

/**
 * @file
 * D3 module file for creating visualizations with d3.js.
 *
 * Sponsored by http://www.freeflowdigital.com
 */

/**
 * Implements hook_menu().
 */
function d3_menu() {
  $items['admin/config/d3'] = array(
    'title' => 'D3',
    'description' => 'Configuration for d3.',
    'position' => 'right',
    'weight' => 20,
    'page callback' => 'system_admin_menu_block_page',
    'access arguments' => array(
      'access administration pages',
    ),
    'file path' => drupal_get_path('module', 'system'),
    'file' => 'system.admin.inc',
  );
  $items['admin/config/d3/settings'] = array(
    'title' => 'D3 Settings',
    'description' => 'Configuration settings for D3',
    'page callback' => 'drupal_get_form',
    'page arguments' => array(
      'd3_config_form',
    ),
    'access arguments' => array(
      'administer d3',
    ),
    'file' => 'd3.admin.inc',
  );
  return $items;
}

/**
 * Implements hook_permission().
 */
function d3_permission() {
  return array(
    'administer d3' => array(
      'title' => t('Administer d3'),
    ),
  );
}

/**
 * Implements hook_theme().
 */
function d3_theme() {
  foreach (libraries_info() as $path => $library) {

    // Call this function to get all fields - i.e. library path.
    $library = libraries_detect($path);

    // If a template was specified in the .info file or hook_libraries_info.
    if (isset($library['template'])) {

      // Change d3.[library name] to d3_[library name] for a theme key.
      $theme_key = str_replace('.', '_', $library['machine name']);
      $themes[$theme_key] = array(
        'variables' => array(),
        'template' => $library['template'],
        'path' => $library['library path'],
      );
    }
  }
  $themes['d3'] = array(
    'variables' => array(
      'type' => NULL,
      'library' => array(),
      'vis_id' => NULL,
    ),
    'template' => 'd3',
  );
  $themes['d3_graphapi'] = array(
    'variables' => array(
      'graph' => NULL,
      'config' => NULL,
    ),
  );
  return $themes;
}

/**
 * Lists library versions by polling CDN
 */
function _d3_list_library_versions_from_cdn() {
  $library_versions = array();
  if (checkdnsrr('d3js.org')) {
    stream_context_set_default(array(
      'https' => array(
        'method' => 'HEAD',
      ),
    ));
    for ($library_version = 3; $library_version < 50; ++$library_version) {
      $hdrs = get_headers("https://d3js.org/d3.v{$library_version}.min.js");
      if ($hdrs) {
        if (substr($hdrs[0], 9, 1) === '2') {
          $library_versions[] = "{$library_version}";
          continue;
        }
      }
      if (count($library_versions) != 0) {
        break;
      }
    }
    stream_context_set_default(array(
      'https' => array(
        'method' => 'GET',
      ),
    ));
  }
  return $library_versions;
}

/**
 * Lists library versions nominally available in sites/all/libraries/d3/
 */
function _d3_list_library_versions_from_libraries() {
  $library_versions = array();
  $path = libraries_get_path('d3');
  if ($path) {
    $files = array();

    // In the repository the files might be named d3.js and d3.min.js.
    $files += file_scan_directory($path, '/d3.js|d3.min.js/');

    // They could also have the version # in the file name.
    $files += file_scan_directory($path, '/d3.v[0-9]+(.min)?.js/');
    foreach ($files as $file) {
      $library_version = '';

      // Assume files named d3.js or d3.min.js are version 3
      if ($file->filename == 'd3.js' || $file->filename == 'd3.min.js') {
        $library_version = 3;
      }
      elseif (preg_match('/d3.v([0-9]+)(.min)?.js/', $file->filename, $matches)) {
        $library_version = $matches[1];
      }
      if ($library_version && !in_array($library_version, $library_versions)) {
        $library_versions[] = "{$library_version}";
      }
    }
  }
  sort($library_versions, SORT_NUMERIC);
  return $library_versions;
}

/**
 * Implements hook_preprocess_d3() for d3.tpl.php.
 */
function d3_preprocess_d3(&$variables) {

  // Let drupal_attributes render this.
  $variables['classes_array'][] = $variables['type'];
  $variables['attributes_array']['id'] = array(
    $variables['vis_id'],
  );
  $library = isset($variables['library']) ? $variables['library'] : FALSE;

  // Let's look for a template file in the library definition.
  if (isset($library['template'])) {
    $variables['theme_hook_suggestion'] = isset($library['library name']) ? $library['library name'] : str_replace('.', '_', $library['machine name']);
  }
}

/**
 * Implements hook_libraries_info_file_paths().
 */
function d3_libraries_info_file_paths() {

  // Get all library directories.
  $libraries = libraries_get_libraries();
  $paths = array();

  // Output an array of paths to check for.
  foreach ($libraries as $path) {
    $paths[] = $path;
  }

  // Load the directory where the d3 example libraries are.
  $example_path = drupal_get_path('module', 'd3') . '/libraries/';

  // Add these to the search directories for libraries.
  foreach (d3_default_libraries() as $example) {
    $paths[] = $example_path . $example;
  }
  return $paths;
}

/**
 * Library post-detect callback to process info files before caching.
 *
 * @see libraries_detect()
 * @see libraries_load()
 */
function d3_detect_library_info(&$library, $version = NULL, $variant = NULL) {
  $handlers = d3_get_library_info_handlers_info();
  foreach ($handlers as $type => $info) {
    $handler = d3_get_library_info_handler($type);
    $handler
      ->setLibrary($library);
    $handler->processor
      ->process();
  }

  // Cycle through d3 libraries and process their info files.
}

/**
 * Get a handler object for a library.
 *
 * @param string $type
 *   The handler type. i.e. 'views'.
 *
 * @return $object
 *   Instance of D3LibraryInfoController
 *
 * @see hook_library_info_handlers()
 */
function d3_get_library_info_handler($key = 'default') {
  static $handlers;
  if (!$handlers) {
    $handlers = d3_get_library_info_handlers_info();
  }
  $handler = !empty($handlers[$key]) ? $handlers[$key] : $handlers['default'];
  if (class_exists($handler['controller'])) {

    // Instantiate controller class.
    $controller = new $handler['controller']($handler);
    if ($controller instanceof D3LibraryInfoController) {

      // These are the info file keys that will be processed.
      $controller->processor
        ->setKeys(array(
        $key,
      ));
    }
  }
  return $controller;
}

/**
 * Get information on all info handlers.
 *
 * @return
 *   Array of library info handler info arrays.
 */
function d3_get_library_info_handlers_info() {
  $handlers = array(
    'default' => array(
      'processor' => 'D3LibraryInfoProcessor',
      'controller' => 'D3LibraryInfoController',
      'mapping' => 'D3DataMapping',
    ),
  );

  // Allow submodules of d3 to define new library info handlers.
  $handlers += module_invoke_all('library_info_handlers');

  // Allow submodules of d3 to alter current info handlers.
  drupal_alter('library_info_handlers', $handlers);
  return $handlers;
}

/**
 * Implements hook_libraries_info_alter().
 */
function d3_libraries_info_alter(&$libraries) {
  foreach (d3_get_libraries() as $library_name => $library) {

    // Automatically add in the d3.drupal dependency so that each
    // d3.library doesn't have to.
    $libraries[$library_name]['dependencies'][] = 'd3.drupal';

    // Add in a function to process additional info file information.
    $libraries[$library_name]['callbacks']['post-detect'][] = 'd3_detect_library_info';
  }

  // By default, the libraries module only checks the libraries folders.
  // We need to add this module's libraries path to the library info.
  $path = drupal_get_path('module', 'd3') . '/libraries/';
  foreach (d3_default_libraries() as $library_name) {

    // Change library path to path/to/module/d3/libraries
    $libraries[$library_name]['library path'] = $path . $library_name;
  }
}

/**
 * Implements hook_libraries_info().
 */
function d3_libraries_info() {
  $libraries = array();

  // Drupal ext adds behaviors and d3 global object.
  $libraries['d3.drupal'] = array(
    'name' => 'D3 Drupal ext',
    'vendor url' => 'http://drupal.org/sandbox/asherry/1477334',
    'files' => array(
      'js' => array(
        'd3.js',
      ),
    ),
    'path' => 'js',
    'library path' => drupal_get_path('module', 'd3'),
    'dependencies' => array(
      'd3',
    ),
    'version' => '1',
  );
  $lib = variable_get('d3_library_source', 'lib');
  $ver = variable_get('d3_library_version', 3);
  if ($lib == 'cdn') {
    $options = array(
      'lines' => 4,
      'file' => "https://d3js.org/d3.v{$ver}.min.js",
    );
    $libraries['d3'] = array(
      'name' => 'D3',
      'vendor url' => 'https://d3js.org/',
      'download url' => 'https://d3js.org/',
      // This will bypass the check for file_exists.
      'installed' => TRUE,
      // For easy reference later.
      'cdn' => TRUE,
      'library path' => 'https://d3js.org',
      'files' => array(
        'js' => array(
          'data' => "d3.v{$ver}.min.js",
        ),
      ),
      'version' => $ver,
    );
    return $libraries;
  }

  // Path to library, (if installed).
  $path = libraries_get_path('d3');
  if ($path) {
    $files = array();

    // In the repository the files might me named d3.js and d3.min.js.
    $files += file_scan_directory($path, '/d3.js|d3.min.js/');

    // They could also have the version # in the file name.
    $files += file_scan_directory($path, '/d3.v[0-9](.min)?.js/');

    // If we don't have any files we shouldn't add the library at all.
    // This will fire drupal error and direct the user to reports.
    if (count($files) == 0) {
      return $libraries;
    }
    $file = NULL;

    // Find the file that matches the specified version.
    foreach ($files as $a_file) {

      // Assume files named d3.js or d3.min.js are version 3
      if ($ver === 3 && ($a_file->filename === "d3.js" || $a_file->filename === "d3.min.js") || $a_file->filename === "d3.v{$ver}.js" || $a_file->filename === "d3.v{$ver}.min.js") {
        $file = $a_file;
      }
    }
    if (!$file) {
      $file = array_shift($files);
    }
    $libraries['d3'] = array(
      'name' => 'D3',
      'vendor url' => 'https://d3js.org/',
      'download url' => 'https://d3js.org/',
      'files' => array(
        'js' => array(
          $file->filename,
        ),
      ),
      'version' => $ver,
    );
  }
  return $libraries;
}

/**
 * Helper callback to return all sample libraries located inside this module.
 */
function d3_default_libraries() {
  return array(
    'd3.columnchart',
    'd3.extend',
    'd3.forcedirected',
    'd3.module_dependencies',
    'd3.linegraph',
    'd3.tooltip',
    'd3.barchart',
    'd3.piechart',
  );
}

/**
 * D3's main API callback.
 *
 * @param array $vis
 *   The chart data
 *
 * @return string
 *   Rendered chart html
 */
function d3_draw($vis) {

  // Type is required for correct processing.
  $type = !empty($vis['type']) ? strtolower($vis['type']) : FALSE;
  if (!$type) {
    drupal_set_message(t('No chart type specified'), 'error');
    return;
  }

  // Ensure we have d3 installed, don't just show a blank screen.
  $d3 = libraries_load('d3');
  if (empty($d3['installed'])) {
    drupal_set_message(t('The d3 library is not correctly installed'), 'error');
    return;
  }
  $output = array(
    '#theme' => 'd3',
  );

  // If user selected cdn, we need to load this manually.
  // @see http://drupal.org/node/864376
  if (!empty($d3['cdn'])) {
    $output['#attached']['js'][] = array(
      'data' => $d3['library path'] . '/' . $d3['files']['js']['data'],
      'type' => 'external',
      'group' => JS_LIBRARY,
    );
  }

  // Form library name - convention is going to be d3.[library].
  $library_name = 'd3.' . $type;

  // Check for library instance / definition.
  if ($lib = libraries_load($library_name)) {

    // Enforce lowercase change.
    $output['#library'] = $lib + array(
      'library name' => str_replace('.', '_', $library_name),
    );
    $output['#type'] = $type;

    // Store as vis_id because key ['id'] will get overridden.
    $output['#vis_id'] = $vis['id'];

    // Now add to the behaviors to execute on page load.
    $js = array(
      'd3' => array(
        'inventory' => array(
          $vis['id'] => $vis,
        ),
      ),
    );
    $output['#attached']['js'][] = array(
      'data' => $js,
      'type' => 'setting',
    );

    // Renders the html - .tpl.php if specified or default d3.tpl.php.
    return render($output);
  }
  else {
    drupal_set_message(t('Invalid chart type %type and/or library %library_name is not installed', array(
      '%type' => $type,
      '%library_name' => $library_name,
    )));
    return '';
  }
}

/**
 * Provides an array of d3 libraries.
 *
 * D3 libraries are going to have a prefix of d3., see README.txt
 * for information on creating a custom d3 library.
 */
function d3_get_libraries() {
  static $d3_libraries;
  if ($d3_libraries) {
    return $d3_libraries;
  }

  // Returns all libraries in the default folders.
  $libraries = libraries_info();
  foreach ($libraries as $library) {
    $library_name = $library['machine name'];

    // Filter out any other non-d3 library. All d3 libraries should have
    // the prefix "d3.".
    if (strpos($library_name, 'd3.') === FALSE) {
      continue;
    }

    // Do not list these default extension libraries.
    if (in_array($library_name, array(
      'd3.extend',
      'd3.tooltip',
      'd3.drupal',
    ))) {
      continue;
    }
    $d3_libraries[$library_name] = $library;
    $d3_libraries[$library_name]['js callback'] = str_replace('d3.', '', $library_name);
  }
  return $d3_libraries;
}

/**
 * Implements hook_graphapi_formats().
 */
function d3_graphapi_formats() {
  return array(
    'd3' => t('D3'),
  );
}

/**
 * Implements hook_graphapi_settings_form().
 */
function d3_graphapi_settings_form($values) {
  $engine = 'd3';
  $form[$engine] = array(
    '#type' => 'fieldset',
    '#title' => 'D3 settings',
  );
  $options = array();
  foreach (d3_get_libraries() as $library) {
    $options[$library['js callback']] = $library['name'];
  }
  $form[$engine]['library'] = array(
    '#title' => 'Default library',
    '#description' => t('Use this d3 library as the default to render graphapi visualizations'),
    '#type' => 'select',
    '#options' => $options,
    '#default_value' => isset($values['library']) ? $values['library'] : key($options),
  );
  return $form;
}

/**
 * Converts $graph data array two arrays for links and nodes.
 *
 * @param array $graph
 *   Associative array of nodes with link information and data.
 *
 * @return array
 *   Links and nodes.
 */
function _d3_graphapi_format_graph_data($graph) {
  $data = array();
  $indices = array();
  $index = 0;
  foreach ($graph as $id => $node) {
    $indices[$id] = $index;
    $index++;
  }

  // Add in edges.
  foreach ($graph as $id => $node) {
    $index = $indices[$id];
    $data['nodes'][$index] = array(
      'name' => $node['data']['title'],
      'group' => isset($node['data']['group']) ? $node['data']['group'] : 1,
      'data' => $node['data'],
    );
    if (count($node['edges']) > 0) {
      foreach ($node['edges'] as $edge => $edge_data) {
        $value = isset($edge_data['data']['value']) ? (int) $edge_data['data']['value'] : NULL;
        $data['links'][] = array(
          'data' => isset($edge_data['data']) ? $edge_data['data'] : array(),
          'source' => $index,
          'target' => $indices[$edge],
          // TODO: This is hard coded, it could be dynamic.
          'value' => $value,
        );
      }
    }
  }
  return $data;
}

/**
 * Displays the visualization for graphapi's selected library.
 */
function theme_d3_graphapi($vars) {
  if (empty($vars['graph'])) {
    return '';
  }
  $default_library = variable_get('d3_graphapi_default_library', 'forcedirected');
  $library = empty($vars['config']['library']) ? $default_library : $vars['config']['library'];
  $graph = _d3_graphapi_format_graph_data($vars['graph']);
  $chart = array(
    'id' => $vars['config']['id'],
    'type' => $library,
    'links' => $graph['links'],
    'nodes' => $graph['nodes'],
    'config' => $vars['config'],
  );
  return d3_draw($chart);
}

/**
 * Implements hook_graphapi_default_settings().
 *
 * We reuse the default settings from thejit_forcedirected_default_settings.
 *
 * @see thejit_forcedirected_default_settings()
 * @see views_object::option_definition()
 */
function d3_graphapi_default_settings() {
  return array(
    'd3' => array(
      'library' => array(
        'default' => 'd3.forcedirected',
      ),
    ),
  );
}

Functions

Namesort descending Description
d3_default_libraries Helper callback to return all sample libraries located inside this module.
d3_detect_library_info Library post-detect callback to process info files before caching.
d3_draw D3's main API callback.
d3_get_libraries Provides an array of d3 libraries.
d3_get_library_info_handler Get a handler object for a library.
d3_get_library_info_handlers_info Get information on all info handlers.
d3_graphapi_default_settings Implements hook_graphapi_default_settings().
d3_graphapi_formats Implements hook_graphapi_formats().
d3_graphapi_settings_form Implements hook_graphapi_settings_form().
d3_libraries_info Implements hook_libraries_info().
d3_libraries_info_alter Implements hook_libraries_info_alter().
d3_libraries_info_file_paths Implements hook_libraries_info_file_paths().
d3_menu Implements hook_menu().
d3_permission Implements hook_permission().
d3_preprocess_d3 Implements hook_preprocess_d3() for d3.tpl.php.
d3_theme Implements hook_theme().
theme_d3_graphapi Displays the visualization for graphapi's selected library.
_d3_graphapi_format_graph_data Converts $graph data array two arrays for links and nodes.
_d3_list_library_versions_from_cdn Lists library versions by polling CDN
_d3_list_library_versions_from_libraries Lists library versions nominally available in sites/all/libraries/d3/