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.
}
This diff is collapsed.
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.
else if (typeof settings.link_colors === 'string') {
colorLinks = settings.link_colors;
}
// If we were passed a function, use that.
else if (typeof settings.link_colors === 'function') {
colorLinks = settings.link_colors;
}
// In the chart element, add an SVG tag with the correct classes, width, and
// height; and inside that, a group for the chart itself.
chartCanvas = d3.select('#' + settings.id)
.append('svg').attr('class', 'sankey').attr('width', width).attr('height', height);
// Set up the chart.
chart = chartCanvas.chart(sankeyType);
chart.nodeWidth(nodeWidth)
.nodePadding(nodePadding)
.iterations(iterations)
.spread(spread)
.alignLabel(alignLabel);
// If we were given node colors, use them.
if (typeof colorNodes !== 'undefined') {
chart.colorNodes(colorNodes);
}
// If we were given link colors, use them.
if (typeof colorLinks !== 'undefined') {
chart.colorLinks(colorLinks);
}
// Draw a sankey chart with the data.
chart.draw({nodes: nodes, links: links});
};
})(jQuery, Drupal, d3);
name = D3 Sankey Examples
description = Several functions that outline the different uses of the d3.sankey API.
core = 7.x
package = Visualization
dependencies[] = d3_sankey
<?php
/**
* @file
* Install, update, uninstall the d3_sankey_examples module.
*/
/**
* Implements hook_requirements().
*/
function d3_sankey_examples_requirements($phase) {
$requirements = array();
$t = get_t();
if ($phase === 'runtime') {
// Remind users to disable this module on the live site.
$requirements['d3_sankey_examples'] = array('title' => $t('D3 Sankey Examples module'));
$requirements['d3_sankey_examples']['severity'] = REQUIREMENT_WARNING;
$requirements['d3_sankey_examples']['value'] = $t('Publicly-visible example page at <a href="@link_href"><code>@link_text</code></a>!', array(
'@link_href' => url('d3/examples/sankey'),
'@link_text' => 'd3/examples/sankey',
));
$requirements['d3_sankey_examples']['description'] = $t('The D3 Sankey Examples module is only intended to be used as a developer reference: you should disable it on any site that can be seen by the general public, because it defines an example page that has no access control!');
}
return $requirements;
}
/**
* Installs d3_sankey_examples module.
*/
function d3_sankey_examples_update_7000(&$sandbox) {
// Noop to ensure Drupal stores a schema version in the database.
}
This diff is collapsed.
{
"name": "drupal/d3_sankey_table_group_pp",
"description": "Provides a Sankey preprocessor that links values in the same row, grouping duplicates.",
"type": "drupal-module",
"require-dev": {
"phpunit/phpunit": "5.5.*"
},
"license": "GPL-2.0+",
"require": {}
}
This diff is collapsed.
name = D3 Sankey: Table grouping preprocessor
description = Provides a Sankey preprocessor that links values in the same row, grouping duplicates.
core = 7.x
package = Visualization
dependencies[] = d3_sankey
dependencies[] = composer_manager
dependencies[] = xautoload (>= 7.x-5.0)
<?xml version="1.0" encoding="UTF-8"?>
<phpunit backupGlobals="false"
backupStaticAttributes="false"
colors="true"
convertErrorsToExceptions="true"
convertNoticesToExceptions="true"
convertWarningsToExceptions="true"
processIsolation="false"
stopOnFailure="false"
syntaxCheck="false"
bootstrap="tests/bootstrap.php"
>
<testsuites>
<testsuite name="Unit">
<directory>./tests/src/Unit</directory>
</testsuite>
</testsuites>
</phpunit>
<?php
namespace Drupal\d3_sankey_table_group_pp\Model;
use Drupal\d3_sankey\Model\Link;
/**
* A type of Sankey link that supports strings for source and target.
*/
class KeyedLink extends Link {
/**
* The array key of the node to connect from.
*
* @var string
*/
public $source;
/**
* The array key of the node to connect to.
*
* @var string
*/
public $target;
/**
* A number representing how big the link is.
*
* @var numeric
*/
public $value;
/**
* D3SankeyLink constructor.
*
* @param string $source
* The array index of the node to connect from.
* @param string $target
* The array index of the node to connect to.
* @param int|float $value
* A number representing how big the link is.
*/
public function __construct($source, $target, $value = 1) {
$this->source = (string) $source;
$this->target = (string) $target;
$this->value = is_numeric($value) ? $value + 0 : 1;
}
}
<?php
namespace Drupal\d3_sankey_table_group_pp;
use Drupal\d3_sankey\DrupalCoreAdapter;
use Drupal\d3_sankey\Model\Link;
use Drupal\d3_sankey\Model\Node;
use Drupal\d3_sankey\Model\RawSankeyData;
use Drupal\d3_sankey\PreprocessorInterface;
use Drupal\d3_sankey_table_group_pp\Model\KeyedLink;
/**
* A data preprocessor that links values in the same row, grouping duplicates.
*
* This preprocessor considers each cell in the table to be a node, and adds a
* link between each column in the same row. If any two cells can be represented
* by the same string, they are considered to be the same node.
*
* To use an example, it would transform data like...
*
* Account | LoB | Client
* --------|-----|--------
* Revenue | SLA | One
* Revenue | NW | One
* Revenue | SLA | Two
*
* ... into the following list of nodes...
*
* 1. Revenue
* 2. SLA
* 3. One
* 4. NW
* 5. Two
*
* ... and the following list of links...
*
* 1. Revenue -> SLA
* 2. SLA -> One
* 3. Revenue -> NW
* 4. NW -> One
* 5. SLA -> Two
*
* ... which would result in a Sankey diagram (roughly) like...
*
* ```
* Revenue --- NW --- One
* \ /
* -- SLA -- Two
* ```
*/
class TableGroupingPreprocessor implements PreprocessorInterface {
/**
* A wrapper around Drupal core functions.
*
* @var \Drupal\d3_sankey\DrupalCoreAdapter
*/
private $adapter;
/**
* An associative array of nodes that make up this Sankey diagram.
*
* @var \Drupal\d3_sankey\Model\Node[]
*/
private $nodes;
/**
* An associative array of links that make up this Sankey diagram.
*
* @var \Drupal\d3_sankey_table_group_pp\Model\KeyedLink[]
*/
private $links;
/**
* D3SankeyGroupingPreprocessor constructor.
*
* @param \Drupal\d3_sankey\Model\Node[] $nodes
* An associative array of nodes that make up this Sankey diagram.
* @param \Drupal\d3_sankey_table_group_pp\Model\KeyedLink[] $links
* An associative array of links that make up this Sankey diagram.
* @param \Drupal\d3_sankey\DrupalCoreAdapter $adapter
* A wrapper around Drupal core functions.
*/
public function __construct($nodes = array(), $links = array(), DrupalCoreAdapter $adapter = NULL) {
$this->nodes = $nodes;
$this->links = $links;
$this->adapter = ($adapter) ? $adapter : new DrupalCoreAdapter();
}
/**
* Get the associative array of nodes that make up this Sankey diagram.
*
* @return \Drupal\d3_sankey\Model\Node[]
* An associative array of nodes that make up this Sankey diagram.
*/
public function getNodes() {
return $this->nodes;
}
/**
* Get the associative array of links that make up this Sankey diagram.
*
* @return \Drupal\d3_sankey_table_group_pp\Model\KeyedLink[]
* An associative array of links that make up this Sankey diagram.
*/
public function getLinks() {
return $this->links;
}
/**
* Set the associative array of nodes that make up this Sankey diagram.
*
* @param \Drupal\d3_sankey\Model\Node[] $nodes
* An associative array of nodes that make up this Sankey diagram.
*/
public function setNodes($nodes) {
$this->nodes = $nodes;
}
/**
* Set the associative array of links that make up this Sankey diagram.
*
* @param \Drupal\d3_sankey_table_group_pp\Model\KeyedLink[] $links
* An associative array of links that make up this Sankey diagram.
*/
public function setLinks($links) {
$this->links = $links;
}
/**
* Load a row of data into the table.
*
* @param array $row
* A row of data.
*/
public function ingestRow($row) {
$previous_node_key = NULL;
// Ensure this row is an array.
$row = (array) $row;
// Loop through each column in the row...
foreach ($row as $current_node_label) {
$current_node_key = $this->generateNodeKey($current_node_label);
// If the current node doesn't exist in $this->nodes yet, add it to the
// nodes array.
if (!array_key_exists($current_node_key, $this->nodes)) {
$this->nodes[$current_node_key] = new Node($current_node_label, NULL, $this->adapter);
}
// If there is a reference to the previous node in this row (i.e.: if this
// is not the first column in the row), add a link from the previous node
// to the current node.
if (!is_null($previous_node_key)) {
$link_key = $this->generateLinkKey($previous_node_key, $current_node_key);
// If the current link doesn't exist in $this->links yet, add it.
if (!array_key_exists($link_key, $this->links)) {
$this->links[$link_key] = new KeyedLink($previous_node_key, $current_node_key);
}
// If it does exist, increment the weight.
else {
$this->links[$link_key]->value++;
}
}
// Keep track of this row for the next iteration through the loop.
$previous_node_key = $current_node_key;
}
}
/**
* {@inheritdoc}
*/
public function getRawData() {
$nodes = array();
$nodes_mapping = array();
$links = array();
// GroupingPreprocessor uses keys in $this->nodes as an implementation
// detail, so that we can identify duplicates. But we must not pass that
// implementation detail to RawSankeyData.
foreach ($this->nodes as $string_key => $node) {
// Add the node to the output array and get the new size of the array.
$new_size = array_push($nodes, $node);
// The new size will be the index of the element we just added, plus one.
$numeric_key = $new_size - 1;
// Record the mapping between the string key and the numeric key.
$nodes_mapping[$string_key] = $numeric_key;
}
// GroupingPreprocessor uses an associative array of GroupingLinks instead
// of a numeric array of Links as an implementation detail, so we can
// identify links between nodes, and ensure we don't enter duplicates. But
// we must not pass that implementation detail to RawSankeyData.
foreach ($this->links as $link) {
// Identify the numeric indices of the source and target nodes.
// The typecasts here are provided for clarity to the reader, but are not
// strictly necessary.
$source_index = (int) $nodes_mapping[(string) $link->source];
$target_index = (int) $nodes_mapping[(string) $link->target];
// Add a regular Link with the numeric indices along with the current
// link's value.
$links[] = new Link($source_index, $target_index, $link->value);
}
return new RawSankeyData($nodes, $links);
}
/**
* Generate a key for the Nodes array, given a node label.
*
* @param string $node_label
* A label for a node.
*
* @return string
* A key for the array of nodes.
*/
private static function generateNodeKey($node_label) {
// In theory, this function could hash the key; however, for simplicity, it
// currently just uses the label as the key. This could cause problems if
// the node label is a string longer than PHP's max size for array key
// strings (e.g.: if we were trying to use BLOBs as node labels).
return (string) $node_label;
}
/**
* Generate a key for the Links array, given source and target node keys.
*
* @param string $source_key
* A key for the array of nodes.
* @param string $target_key
* A key for the array of nodes.
*
* @return string
* A key for the array of links.
*/
private static function generateLinkKey($source_key, $target_key) {
// In theory, this function could hash the keys; however, for simplicity, it
// currently just joins the two keys with a '---' in the middle. This could
// cause problems if the combined node labels are a string longer than PHP's
// max size for array key strings (e.g.: if we were trying to use BLOBs as
// node labels).
return (string) $source_key . '---' . (string) $target_key;
}
}
<?php
/**
* @file
* Bootstrap PHPUnit tests.
*
* Since PHPUnit doesn't bootstrap Drupal, and we don't have any way of knowing
* where the xautoload module is in relation to this module, we have to manually
* include the files we need.
*/
require_once __DIR__ . '/../../../src/DrupalCoreAdapter.php';
require_once __DIR__ . '/../../../src/PreprocessorInterface.php';
require_once __DIR__ . '/../../../src/Model/Link.php';
require_once __DIR__ . '/../../../src/Model/Node.php';
require_once __DIR__ . '/../../../src/Model/RawSankeyData.php';
require_once __DIR__ . '/../src/Model/KeyedLink.php';
require_once __DIR__ . '/../src/TableGroupingPreprocessor.php';
<?php
namespace Drupal\Tests\D3SankeyGroupingPreprocess;
use