Commit 467755da authored by M Parker's avatar M Parker

Add an object-oriented API to facilitate creating Sankey data preprocessors.

parent aba8d4ec
......@@ -5,3 +5,4 @@ package = Visualization
dependencies[] = libraries (>=2.0)
dependencies[] = d3
dependencies[] = xautoload (>= 7.x-5.0)
......@@ -76,6 +76,13 @@ 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().
*/
......
<?php
namespace Drupal\d3_sankey;
/**
* Adapter class to provide a mockable wrapper around D7 procedural functions.
*
* Drupal's procedural functions are hard to use in automated unit tests without
* bootstraping Drupal, which is computationally and temporally expensive, and
* would use a lot of memory, especially if you really only need one or two
* functions from Drupal core.
*
* This adapter class provides a thin wrapper around certain Drupal core
* functions which are used in the object-oriented code in this module. These
* functions are intentionally _not_ static, because PHPUnit does not
* (currently) let you mock static functions.
*
* This class is intended to be passed to a class that needs it in the class'
* constructor. The following template is recommended...
*
* ```
* public function __construct(DrupalCoreAdapter $adapter = NULL) {
* // ...
* $this->adapter = ($adapter) ? $adapter : new DrupalCoreAdapter();
* // ...
* }
* ```
*
* ... this allows regular code to call the constructor without providing an
* adapter object. Meanwhile, in the test, you can construct a test double
* (either by extending this object and providing an alternate implementation,
* or by using PHPUnit's mocking tools to create a dummy, stub, fake, spy, or
* mock). For example, the following code defines a fake method that simply
* returns the argument it was passed...
*
* ```
* $this->mockAdapter = $this->createMock(DrupalCoreAdapter::class);
* $this->mockAdapter->method('drupalHtmlId')->willReturnArgument(0);
* ```
*
* If you are writing your own preprocessor, feel free to extend this object if
* your own code makes use of other functions in Drupal core.
*/
class DrupalCoreAdapter {
/**
* Prepares a string for use as a valid HTML ID and guarantees uniqueness.
*
* @see drupal_html_id()
*/
public function drupalHtmlId($id) {
return drupal_html_id($id);
}
}
<?php
namespace Drupal\d3_sankey\Model;
/**
* An object representing a single, raw Sankey link.
*/
class Link {
/**
* The array index of the node to connect from.
*
* @var int
*/
public $source;
/**
* The array index of the node to connect to.
*
* @var int
*/
public $target;
/**
* A number representing how big the link is.
*
* @var numeric
*/
public $value;
/**
* D3SankeyLink constructor.
*
* @param int $source
* The array index of the node to connect from.
* @param int $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 = (int) $source;
$this->target = (int) $target;
$this->value = is_numeric($value) ? $value + 0 : 1;
}
/**
* Get an associated-array version of this link.
*
* @return array
* An associative array containing:
* - source: The array index of the node to connect from.
* - target: The array index of the node to connect to.
* - value (optional): A number representing how big the link is.
*/
public function toAssocArray() {
$answer = array('source' => $this->source, 'target' => $this->target);
if (!is_null($this->value)) {
$answer['value'] = $this->value;
}
return $answer;
}
}
<?php
namespace Drupal\d3_sankey\Model;
use Drupal\d3_sankey\DrupalCoreAdapter;
/**
* An object representing a single, raw Sankey node.
*/
class Node {
/**
* A mockable wrapper around D7 procedural functions.
*
* @var \Drupal\d3_sankey\DrupalCoreAdapter
*/
public $adapter;
/**
* A label for the node.
*
* @var string
*/
public $name;
/**
* An SVG ID attribute (similar to an HTML ID attribute) to add to the node.
*
* @var string
*/
public $id;
/**
* D3SankeyNode constructor.
*
* @param string $name
* A label for the node.
* @param string|null $id
* An SVG ID attribute (similar to an HTML ID attribute) to add to the node.
* @param DrupalCoreAdapter $adapter
* A mockable wrapper around D7 procedural functions.
*/
public function __construct($name, $id = NULL, DrupalCoreAdapter $adapter = NULL) {
$this->adapter = ($adapter) ? $adapter : new DrupalCoreAdapter();
$this->name = (string) $name;
$this->id = ($id) ? (string) $id : $this->adapter->drupalHtmlId($name);
}
/**
* Get an associated-array version of this node.
*
* @return array
* An associative array containing:
* - name: A label for the node.
* - id (optional): An SVG ID attribute (similar to an HTML ID attribute) to
* add to the node.
*/
public function toAssocArray() {
$answer = array('name' => $this->name);
if (!is_null($this->id)) {
$answer['id'] = $this->id;
}
return $answer;
}
}
<?php
namespace Drupal\d3_sankey\Model;
/**
* A container class for raw Sankey data.
*
* The raw node and link data used by the Sankey chart plugin for the D3 library
* is very closely related: if you add, rearrange, or delete nodes, you also
* have to change all the links, and vice-versa.
*
* This close inter-relatedness makes the data difficult to work with. It would
* be useful to be able to create some sort of preprocessor to turn data that is
* easier to work with into the raw data that the Sankey chart plugin for the D3
* library needs.
*
* In order to do make a preprocessor, we need a way to return the nodes and
* links _at the same time_, in a single chunk (a 2-tuple). Besides helping to
* avoid situations where the Sankey data changes between the time when you get
* the links and the time when you get the nodes, it makes preprocessor
* implementation easier by allowing them both to be calculated at the same
* time.
*
* This class is designed to contain both the node and link data in a single
* chunk, with a few extra convenience functions.
*/
class RawSankeyData {
/**
* An array of nodes that make up this Sankey diagram.
*
* @var \Drupal\d3_sankey\Model\Node[]
*/
private $nodes;
/**
* An array of links that make up this Sankey diagram.
*
* @var \Drupal\d3_sankey\Model\Link[]
*/
private $links;
/**
* RawSankeyData constructor.
*
* @param \Drupal\d3_sankey\Model\Node[] $nodes
* An array of nodes that make up this Sankey diagram.
* @param \Drupal\d3_sankey\Model\Link[] $links
* An array of links that make up this Sankey diagram.
*/
public function __construct($nodes = array(), $links = array()) {
$this->nodes = $nodes;
$this->links = $links;
}
/**
* Get an array of nodes that make up this Sankey diagram.
*
* @return \Drupal\d3_sankey\Model\Node[]
* An array of nodes that make up this Sankey diagram.
*/
public function getNodes() {
return $this->nodes;
}
/**
* Get an array of associative arrays representing the nodes in this raw data.
*
* @return array
* A numeric array of associative arrays containing:
* - name: A label for the node.
* - id (optional): An SVG ID attribute (similar to an HTML ID attribute) to
* add to the node.
*/
public function getAssocArrayNodes() {
$answer = array();
foreach ($this->nodes as $node) {
$answer[] = $node->toAssocArray();
}
return $answer;
}
/**
* Get an array of links that make up this Sankey diagram.
*
* @return \Drupal\d3_sankey\Model\Link[]
* An array of links that make up this Sankey diagram.
*/
public function getLinks() {
return $this->links;
}
/**
* Get an array of associative arrays representing the links in this raw data.
*
* @return array
* A numeric array of associative arrays containing:
* - name: A label for the node.
* - id (optional): An SVG ID attribute (similar to an HTML ID attribute) to
* add to the node.
*/
public function getAssocArrayLinks() {
$answer = array();
foreach ($this->links as $link) {
$answer[] = $link->toAssocArray();
}
return $answer;
}
/**
* Set the array of nodes that make up this Sankey diagram.
*
* @param \Drupal\d3_sankey\Model\Node[] $nodes
* An array of nodes that make up this Sankey diagram.
*/
public function setNodes($nodes) {
$this->nodes = $nodes;
}
/**
* Set the array of nodes that make up this Sankey diagram.
*
* @param \Drupal\D3Sankey\Model\Link[] $links
* An array of links that make up this Sankey diagram.
*/
public function setLinks($links) {
$this->links = $links;
}
}
<?php
namespace Drupal\d3_sankey;
/**
* An interface for objects that transform other data into raw Sankey data.
*
* The raw node and link data used by the Sankey chart plugin for the D3 library
* is very closely related: if you add, rearrange, or delete nodes, you also
* have to change all the links, and vice-versa.
*
* This close inter-relatedness makes the data difficult to work with. It would
* be useful to be able to create some sort of preprocessor to turn data that is
* easier to work with into the raw data that the Sankey chart plugin for the D3
* library needs.
*
* This interface provides a basic template for a preprocessor. It purposely
* does not include any templates for functions to input data into the
* preprocessor: those could vary widely depending on the data that needs to be
* preprocessed (e.g.: some data might be in a tabular form, other data might be
* linked-lists, etc.).
*/
interface PreprocessorInterface {
/**
* Get the raw data that makes up this Sankey diagram.
*
* @return \Drupal\d3_sankey\Model\RawSankeyData
* The raw data that makes up this Sankey diagram.
*/
public function getRawData();
}
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment