Commit 5bc6c20d authored by Tom Graham's avatar Tom Graham

initial commit

parents
{
"extends": "eslint:recommended",
"env": {
"browser": true
},
"globals": {
"Drupal": true,
"drupalSettings": true,
"drupalTranslations": true,
"domready": true,
"jQuery": true,
"_": true,
"matchMedia": true,
"Backbone": true,
"Modernizr": true,
"CKEDITOR": true
},
"rules": {
// Errors.
"array-bracket-spacing": [2, "never"],
"block-scoped-var": 2,
"brace-style": [2, "stroustrup", {"allowSingleLine": true}],
"comma-dangle": [2, "never"],
"comma-spacing": 2,
"comma-style": [2, "last"],
"computed-property-spacing": [2, "never"],
"curly": [2, "all"],
"eol-last": 2,
"eqeqeq": [2, "smart"],
"guard-for-in": 2,
"indent": [2, 2, {"SwitchCase": 1}],
"key-spacing": [2, {"beforeColon": false, "afterColon": true}],
"keyword-spacing": [2, {"before": true, "after": true}],
"linebreak-style": [2, "unix"],
"lines-around-comment": [2, {"beforeBlockComment": true, "afterBlockComment": false}],
"new-parens": 2,
"no-array-constructor": 2,
"no-caller": 2,
"no-catch-shadow": 2,
"no-eval": 2,
"no-extend-native": 2,
"no-extra-bind": 2,
"no-extra-parens": [2, "functions"],
"no-implied-eval": 2,
"no-iterator": 2,
"no-label-var": 2,
"no-labels": 2,
"no-lone-blocks": 2,
"no-loop-func": 2,
"no-multi-spaces": 2,
"no-multi-str": 2,
"no-native-reassign": 2,
"no-nested-ternary": 2,
"no-new-func": 2,
"no-new-object": 2,
"no-new-wrappers": 2,
"no-octal-escape": 2,
"no-process-exit": 2,
"no-proto": 2,
"no-return-assign": 2,
"no-script-url": 2,
"no-sequences": 2,
"no-shadow-restricted-names": 2,
"no-spaced-func": 2,
"no-trailing-spaces": 2,
"no-undef-init": 2,
"no-undefined": 2,
"no-unused-expressions": 2,
"no-unused-vars": [2, {"vars": "all", "args": "none"}],
"no-with": 2,
"object-curly-spacing": [2, "never"],
"one-var": [2, "never"],
"quote-props": [2, "consistent-as-needed"],
"quotes": [2, "single", "avoid-escape"],
"semi": [2, "always"],
"semi-spacing": [2, {"before": false, "after": true}],
"space-before-blocks": [2, "always"],
"space-before-function-paren": [2, {"anonymous": "always", "named": "never"}],
"space-in-parens": [2, "never"],
"space-infix-ops": 2,
"space-unary-ops": [2, { "words": true, "nonwords": false }],
"spaced-comment": [2, "always"],
"strict": 2,
"yoda": [2, "never"],
// Warnings.
"max-nested-callbacks": [1, 3],
"valid-jsdoc": [1, {
"prefer": {
"returns": "return",
"property": "prop"
},
"requireReturn": false
}]
}
}
name = D3 Sankey
description = Creates sankey diagrams with D3 library.
core = 7.x
package = Visualization
dependencies[] = libraries (>=2.0)
dependencies[] = d3
dependencies[] = xautoload (>= 7.x-5.0)
<?php
/**
* @file
* Install, update, uninstall the d3_sankey module.
*/
/**
* Implements hook_requirements().
*/
function d3_sankey_requirements($phase) {
$requirements = array();
$t = get_t();
if ($phase === 'runtime') {
// A requirements row for the d3-plugins-sankey library.
$requirements['d3_plugins_sankey'] = array('title' => $t('d3-sankey'));
$library = libraries_detect('d3-plugins-sankey');
if ($library['installed']) {
$requirements['d3_plugins_sankey']['severity'] = REQUIREMENT_OK;
$requirements['d3_plugins_sankey']['value'] = $library['version'];
}
else {
$requirements['d3_plugins_sankey']['severity'] = REQUIREMENT_ERROR;
$requirements['d3_plugins_sankey']['value'] = $library['error'];
$requirements['d3_plugins_sankey']['description'] = $library['error message'];
}
// A requirements row for the d3.chart library.
$requirements['d3_charts'] = array('title' => $t('D3: Chart'));
$library = libraries_detect('d3.chart');
if ($library['installed']) {
$requirements['d3_charts']['severity'] = REQUIREMENT_OK;
$requirements['d3_charts']['value'] = $library['version'];
}
else {
$requirements['d3_charts']['severity'] = REQUIREMENT_ERROR;
$requirements['d3_charts']['value'] = $library['error'];
$requirements['d3_charts']['description'] = $library['error message'];
}
// A requirements row for the d3.chart.sankey library.
$requirements['d3_charts_sankey'] = array('title' => $t('D3: Sankey'));
$library = libraries_detect('d3.chart.sankey');
if ($library['installed']) {
$requirements['d3_charts_sankey']['severity'] = REQUIREMENT_OK;
$requirements['d3_charts_sankey']['value'] = $library['version'];
}
else {
$requirements['d3_charts_sankey']['severity'] = REQUIREMENT_ERROR;
$requirements['d3_charts_sankey']['value'] = $library['error'];
$requirements['d3_charts_sankey']['description'] = $library['error message'];
}
}
return $requirements;
}
/**
* Installs d3_sankey module.
*/
function d3_sankey_update_7000(&$sandbox) {
// Noop to ensure Drupal stores a schema version in the database.
}
<?php
/**
* @file
* Hooks and helper functions for the d3_sankey module.
*
* To get the D3 module to recognize that this module defines a plugin for it,
* we need a JS library that begins with 'd3.', contains no further dots in the
* name, and depends on the 'd3-plugins-sankey', 'd3.chart', and
* 'd3.chart.sankey' libraries we define in d3_sankey_libraries_info().
*
* From experience, the 'd3.chart.sankey' module won't work because it contains
* an extra dot in the name. So we have to make a dummy library.
*/
/* Constants. */
/**
* A type of Sankey chart that does not select nodes or links on hover.
*
* Set the 'sankeyType' setting to this value in order to create a chart of this
* type. This type will also be selected if a 'sankeyType' is not specified.
*/
define('D3_SANKEY_SANKEYTYPE_DEFAULT', 'Sankey');
/**
* A type of Sankey chart that only selects the current node or link on hover.
*
* Set the 'sankeyType' setting to this value in order to create a chart of this
* type.
*/
define('D3_SANKEY_SANKEYTYPE_SELECTION', 'Sankey.Selection');
/**
* A type of Sankey chart that selects the current node/link and its siblings.
*
* Set the 'sankeyType' setting to this value in order to create a chart of this
* type.
*/
define('D3_SANKEY_SANKEYTYPE_PATH', 'Sankey.Path');
/**
* A setting to align node labels automatically.
*
* You probably want this alignment unless you are prepared to write custom CSS
* to handle labels overflowing one side of the diagram and being clipped.
*
* Set the 'alignLabel' setting to this value in order to create a chart of this
* type. This type will also be selected if a 'alignLabel' is not specified.
*/
define('D3_SANKEY_ALIGNLABEL_AUTO', 'auto');
/**
* A setting to align node labels to the start of paths connecting from them.
*
* In LTR languages, this setting will result in the labels for the right-most
* column of nodes overflowing on the right-hand side of the chart; default CSS
* causes these labels to be clipped (i.e.: not visible).
*
* Set the 'alignLabel' setting to this value in order to create a chart of this
* type.
*/
define('D3_SANKEY_ALIGNLABEL_START', 'start');
/**
* A setting to align node labels to the end of paths connecting to them.
*
* In LTR languages, this setting will result in the labels for the left-most
* column of nodes overflowing on the left-hand side of the chart; default CSS
* causes these labels to be clipped (i.e.: not visible).
*
* Set the 'alignLabel' setting to this value in order to create a chart of this
* type.
*/
define('D3_SANKEY_ALIGNLABEL_END', 'end');
/* Hooks. */
/**
* Implements hook_xautoload().
*/
function d3_sankey_xautoload($adapter) {
$adapter->absolute()->addPsr4('Drupal\d3_sankey', 'src');
}
/**
* Implements hook_libraries_info().
*/
function d3_sankey_libraries_info() {
$libraries = array();
// The d3-plugins-sankey library: New Relic's fork of the default D3 Sankey
// plugin. New Relic's fork displays nodes even if they don't have links to
// them, has Bower support, interpolated numbers, and uses the D3 API more
// consistently.
$libraries['d3-plugins-sankey'] = array(
'name' => 'd3-sankey',
'vendor url' => 'https://github.com/newrelic-forks/d3-plugins-sankey',
'download url' => 'https://github.com/newrelic-forks/d3-plugins-sankey/releases',
'download file url' => 'https://github.com/newrelic-forks/d3-plugins-sankey/archive/v1.1.0.tar.gz',
'version arguments' => array(
'file' => 'bower.json',
'pattern' => '@"version"[\s]*:[\s]*"([0-9a-zA-Z\.-]+)"@',
),
'files' => array(
'js' => array('sankey.js'),
),
'dependencies' => array('d3'),
);
// The d3.chart library.
$libraries['d3.chart'] = array(
'name' => 'd3.chart',
'vendor url' => 'https://github.com/misoproject/d3.chart',
'download url' => 'https://github.com/misoproject/d3.chart/releases',
'download file url' => 'https://github.com/misoproject/d3.chart/archive/v0.2.1.tar.gz',
'version arguments' => array(
'file' => 'd3.chart.js',
'pattern' => '@d3\.chart[\s]+-[\s]+v([0-9a-zA-Z\.-]+)@',
),
'files' => array(
'js' => array('d3.chart.js'),
),
'variants' => array(
'minified' => array(
'js' => array('d3.chart.min.js'),
),
),
'dependencies' => array('d3'),
);
// The d3.chart.sankey library.
$libraries['d3.chart.sankey'] = array(
'name' => 'd3.chart.sankey',
'vendor url' => 'https://github.com/q-m/d3.chart.sankey',
'download url' => 'https://github.com/q-m/d3.chart.sankey/releases',
'download file url' => 'https://github.com/q-m/d3.chart.sankey/archive/v0.2.0.tar.gz',
'version arguments' => array(
'file' => 'd3.chart.sankey.js',
'pattern' => '@d3\.chart\.sankey[\s]+-[\s]+v([0-9a-zA-Z\.-]+)@',
),
'files' => array(
'js' => array('d3.chart.sankey.js'),
),
'variants' => array(
'minified' => array(
'js' => array('d3.chart.sankey.min.js'),
),
),
'dependencies' => array('d3-plugins-sankey', 'd3.chart'),
'integration files' => array(
'd3_charts_sankey' => array(
'js' => array('d3_charts_sankey.js'),
),
),
);
return $libraries;
}
/**
* Implements hook_libraries_info_file_paths().
*/
function d3_sankey_libraries_info_file_paths() {
$paths = array();
// See the file doc comment for context. Since we are defining a dummy JS
// library that exists solely for the purpose of making this module work with
// the D3 Drupal module, and also because D3 expects us to include it in this
// module, we need to tell the Libraries API module where to find the library
// file.
$paths[] = _d3_sankey_js_library_path_prefix() . 'd3.sankey';
return $paths;
}
/**
* Implements hook_libraries_info_alter().
*/
function d3_sankey_libraries_info_alter(&$libraries) {
// See the file doc comment for context. By a convention set by the D3 Drupal
// module and also used by the d3_charts module (a separate project that also
// defines a plugin for the D3 Drupal module), we are putting the
// .libraries.info file that defines a library to the Libraries API inside the
// folder that contains the library. So we also have to tell the Libraries API
// where to look for the .libraries.info file.
$libraries['d3.sankey']['library path'] = _d3_sankey_js_library_path_prefix()
. 'd3.sankey';
}
/* Form element validation callbacks. */
/**
* Form element validation callback: Validate a required list of colors.
*
* @param array $element
* The form element to validate.
* @param array $form_state
* The current state of the form.
*/
function d3_sankey_element_validate_required_color_list($element, &$form_state) {
$invalid_color_strings = array();
$value = $element['#value'];
// Parse the element value into a list of colors.
$colors = _d3_sankey_explode_csv_nsv_list((string) $value);
// Validate each color. If we find an invalid one, keep track of it so we can
// write a unified error message at the end.
foreach ($colors as $color) {
if (!_d3_sankey_color_string_is_valid((string) $color)) {
$invalid_color_strings[] = (string) $color;
}
}
// Set a form element error if we found any invalid color strings.
$num_invalid_color_strings = count($invalid_color_strings);
if ($num_invalid_color_strings > 0) {
$error_message = format_plural($num_invalid_color_strings,
'Invalid color string %colors.',
'Invalid color strings %colors.',
array(
'%colors' => implode(', ', $invalid_color_strings),
));
form_error($element, $error_message);
}
}
/**
* Form element validation callback: Validate an optional list of colors.
*
* @param array $element
* The form element to validate.
* @param array $form_state
* The current state of the form.
*/
function d3_sankey_element_validate_optional_color_list($element, &$form_state) {
$value = $element['#value'];
// Special case: if the value is empty, return without doing anything.
if (empty($value)) {
return;
}
// If we get here, the color list is not empty and should be validated
// normally.
return d3_sankey_element_validate_required_color_list($element, $form_state);
}
/* Helper functions. */
/**
* Helper function to return an array of Sankey chart type options.
*
* The output from this function is intended to be used as the '#options'
* property for Form API 'checkboxes', 'radios', 'select', and 'tableselect'
* controls.
*
* @return array
* An associative array, where the key is a string representing the type of
* Sankey chart, and the value is a string containing a translated label for
* that type of Sankey chart.
*/
function d3_sankey_options_sankeytype() {
return array(
D3_SANKEY_SANKEYTYPE_DEFAULT => t('Default'),
D3_SANKEY_SANKEYTYPE_SELECTION => t('Current item selection'),
D3_SANKEY_SANKEYTYPE_PATH => t('Current item and path selection'),
);
}
/**
* Helper function to return an array of Sankey label alignment options.
*
* The output from this function is intended to be used as the '#options'
* property for Form API 'checkboxes', 'radios', 'select', and 'tableselect'
* controls.
*
* @return array
* An associative array, where the key is a string representing the alignment
* for node labels in a Sankey chart, and the value is a string containing a
* translated label for that type of label alignment.
*/
function d3_sankey_options_alignlabel() {
return array(
D3_SANKEY_ALIGNLABEL_AUTO => t('Automatically align labels'),
D3_SANKEY_ALIGNLABEL_START => t('Align labels to start of paths'),
D3_SANKEY_ALIGNLABEL_END => t('Align labels to end of paths'),
);
}
/**
* Helper function to return the path to the libraries contained in this module.
*
* @param bool $reset
* Reset the static cache of the path.
*
* @return string
* A string representing the path to the libraries contained in this module.
*/
function _d3_sankey_js_library_path_prefix($reset = FALSE) {
$path = &drupal_static(__FUNCTION__);
if (!isset($path) || $reset) {
$path = drupal_get_path('module', 'd3_sankey') . '/libraries/';
}
return (string) $path;
}
/**
* Explode a string containing a list of comma- or newline-separated values.
*
* @param $input_string
* A string to parse.
*
* @return array
* An array of sub-strings.
*/
function _d3_sankey_explode_csv_nsv_list($input_string) {
$answer = array();
// Trim the input string.
$input_string = trim((string) $input_string);
// Separate everything that is comma-separated.
$initial_array = explode(',', $input_string);
// Handle the case when explode returns an array with one element (an empty
// string) if passed an empty string.
if (count($initial_array) === 1 && reset($initial_array) === '') {
$initial_array = array();
}
// Loop through the new array and separate everything that is newline-
// separated.
foreach ($initial_array as $substring) {
$sub_array = explode("\n", $substring);
// Handle the case when explode returns an array with one element (an empty
// string) if passed an empty string.
if (count($sub_array) === 1 && reset($sub_array) === '') {
$sub_array = array();
}
foreach ($sub_array as $item) {
$answer[] = trim((string) $item);
}
}
return $answer;
}
/**
* Validate that a string represents a valid color.
*
* Currently only supports hexidecimal strings.
*
* @param string $color
* The string to validate as a color.
*
* @return bool
* TRUE if the color is valid; FALSE otherwise.
*/
function _d3_sankey_color_string_is_valid($color) {
$is_valid = FALSE;
// Validate a hexidecimal color string. See color_valid_hexadecimal_string().
if (preg_match('/^#([a-f0-9]{3}){1,2}$/iD', $color) === 1) {
$is_valid = TRUE;
}
return $is_valid;
}
/**
* Modify a chart definition to include the contents of a color string.
*
* @param array &$chart
* The chart definition to modify.
* @param string $key
* The key to place the color(s) in the chart definition.
* @param string $colors_string
* The color string to parse.
*/
function _d3_sankey_add_colors_to_chart(&$chart, $key, $colors_string) {
$colors_array = _d3_sankey_explode_csv_nsv_list($colors_string);
$num_colors = count($colors_array);
// If we found one color, return it as a string.
if ($num_colors === 1) {
$chart[(string) $key] = (string) array_shift($colors_array);
}
// If we found more than one color, return them in an array.
elseif ($num_colors > 1) {
$chart[(string) $key] = $colors_array;
}
// If we get here, don't touch the chart array, because there were no colors.
}
name = Sankey
files[js][] = sankey.js
files[css][] = sankey.css
version = 0.1
dependencies[] = d3-plugins-sankey
dependencies[] = d3.chart
dependencies[] = d3.chart.sankey
; Unfortunately, raw data integration with d3_views is not possible because:
; - the structure of the nodes[] and links[] arrays doesn't really fit with any
; of the `__data_type`s defined in that module - we would need a something
; similar to a '2dnav' mapper, except that we would need to lock down how many
; keys can be used (1 or 2 for nodes[]; 2 or 3 for links[]) and the keys
; themselves ('name', and 'id' for nodes[]; 'value', 'source', and 'target'
; for links[]);
; - we cannot write a replacement mapper for Sankey charts ourselves because
; neither d3_get_library_info_handler() nor \d3_views_plugin_style_d3::init()
; provides us with a way to override the Views plugin for a specific type of
; chart; and;
; - if we override the original d3_views plugins, we will conflict with other
; modules that need to do the same thing.
.sankey .node rect {
fill-opacity: .9;
shape-rendering: crispEdges;
stroke-width: 0;
}
.sankey .node text {
text-shadow: 0 1px 0 #fff;
}
.sankey .link {
fill: none;
stroke: #000;
stroke-opacity: .2;
}
/**
* @file
* D3 Sankey library.
*/
(function ($, Drupal, d3) {
'use strict';
Drupal.d3.sankey = function (select, settings) {
var chartCanvas;
var chart;
var colorNodes;
var colorLinks;
// Grab settings from the settings object passed to us by the D3 module. Use
// defaults if the settings we want do not exist in the settings object.
var sankeyType = (settings.sankeyType || 'Sankey');
var width = parseInt(settings.width || 700);
var height = parseInt(settings.height || 400);
var nodeWidth = parseInt(settings.nodeWidth || 24);
var nodePadding = parseInt(settings.nodePadding || 8);
var spread = Boolean(settings.spread || true);
var iterations = parseInt(settings.iterations || 1);
var alignLabel = (settings.alignLabel || 'auto');
// Grab data for the chart if it exists in it's raw form; if not, use empty
// arrays for now.
var nodes = (settings.nodes || []);
var links = (settings.links || []);
// If we were passed an array of node colors, create an ordinal scale and
// use that.
if (Array.isArray(settings.node_colors)) {
colorNodes = d3.scale.ordinal().range(settings.node_colors);
}
// If we were passed a single node color, use that.
else if (typeof settings.node_colors === 'string') {
colorNodes = settings.node_colors;
}
// If we were passed a function, use that.
else if (typeof settings.node_colors === 'function') {
colorNodes = settings.node_colors;
}
// If we were passed an array of link colors, create an ordinal scale and
// use that.
if (Array.isArray(settings.link_colors)) {
colorLinks = d3.scale.ordinal().range(settings.link_colors);
}
// If we were passed a single link color, use that.