For visualizing data, D3 rules. To show an example of how to integrate D3 with Drupal 8, let’s take a custom module created in Drupal 7 and refactor it for Drupal 8.
The original D7 module was an exercise in sorting a range of integers from user entered form inputs. To see it in action, install and enable the custom module in a vanilla Drupal 7 site. Here's the module in an animated gif taking the form inputs with D3 handling the graphing/visualization:
The challenge was to take the total number of integers (represented by the purple bars) and generate a random set of numbers ranging from the first delimiting integer to the second delimiting integer. The Step
and Play
buttons show the animation of sorting the randomly generated numbers in descending order.
In Drupal 7, the module file contains the typical form elements and menu callbacks using hook_menu()
. The heavy lifting is done by the custom JavaScript file that leverages the D3 library to load the bar chart and to animate the sorting.
Converting Drupal 7 hook_menu() items
In Drupal 8, we have to convert hook_menu()
to use the Drupal 8’s routing system by adding a MODULE.routing.yml
file. To breakdown the pieces, let’s start with creating a route for the path/callback in a new file called bubblesort.routing.yml
. In this case, there are two routes that need defining: the path to the form itself and a path to the JSON data generated from the form inputs needed by the custom JavaScript.
bubblesort.form:
path: '/bubblesort'
defaults:
_title: 'Bubble Sort'
_form: '\Drupal\bubblesort\Form\BubblesortForm'
requirements:
_permission: 'access content'
bubblesort.bubblesort_json:
path: '/bubblesort/json/{data}'
defaults:
_title: 'Bubble JSON'
_controller: '\Drupal\bubblesort\Controller\BubblesortController::bubblesort_json'
requirements:
_permission: 'access content'
Controllers
Next, let’s take a closer look at the controller classes. The form controller lives in the src/Form
directory and is called BubblesortForm.php
. This code handles how the form is built, validated, and submitted. In Drupal 7, this is the equivalent of a form function passed as a page argument to hook_menu()
.
<?php
namespace Drupal\bubblesort\Form;
use Drupal\Core\Form\FormBase;
use Drupal\Core\Form\FormStateInterface;
/**
* Builds the bubblesort form.
*/
class BubblesortForm extends FormBase {
/**
* {@inheritDoc}
*/
public function getFormId() {
return 'bubblesortform';
}
/**
* {@inheritDoc}
*/
public function buildForm(array $form, FormStateInterface $form_state) {
global $base_url;
// Use hook_theme to generate form render array.
$form['#theme'] = 'bubblesortform';
$form['#attached']['library'][] = 'bubblesort/bubblesort-form';
$form['#attached']['drupalSettings']['baseUrl'] = $base_url;
$form['numbers_total'] = array(
'#type' => 'number',
'#title' => $this->t('Total number of bars:'),
'#required' => true,
'#min' => 1,
'#max' => 35,
);
$form[integer_min] = array(
'#type' => 'number',
'#title' => $this->t('First number:'),
'#required' => TRUE,
'#min' => 1,
'#max' => 99,
);
$form[integer_max] = array(
'#type' => 'number',
'#title' => $this->t('Second number:'),
'#required' => TRUE,
'#min' => 1,
'#max' => 99,
);
$form['submit'] = array(
'#type' => 'submit',
'#value' => $this->t('Shuffle'),
);
$form['step_button'] = array(
'#type' => 'button',
'#value' => $this->t('Step'),
);
$form['play_button'] = array(
'#type' => 'button',
'#value' => $this->t('Play'),
);
return $form;
}
/**
* {@inheritDoc}
*/
public function validateForm(array &$form, FormStateInterface $form_state) {
$values = $form_state->getValues();
if ($values[integer_max] <= $values[integer_min]) {
$form_state->setErrorByName(integer_max, t('Second number must be greater than the first number.'));
}
}
/**
* {@inheritDoc}
*/
public function submitForm(array &$form, FormStateInterface $form_state) {
drupal_set_message($this->t('@total bars between @first and @second', array(
'@total' => $form_state->getValue('numbers_total'),
'@first' => $form_state->getValue(integer_min),
'@second' => $form_state->getValue(integer_max),
)));
$form_state->setRebuild();
}
}
Attaching Libraries
The following two lines from the file above attach the custom JavaScript and CSS to the form and pass the base URL using drupalSettings
(formerly Drupal.settings
in Drupal 7) :
$form['#attached']['library'][] = 'bubblesort/bubblesort-form';
$form['#attached']['drupalSettings']['baseUrl'] = $base_url;
The library is defined by the bubblesort.libraries.yml
file in the root of the module folder. Multiple libraries can be defined as entries detailing dependencies and assets such as CSS and JavaScript files. Here is the bubblesort.libraries.yml
:
bubblesort-form:
version: 1.x
css:
theme:
css/bubblesort.css: {}
js:
js/d3.min.js: {}
js/bubblesort.js: {}
dependencies:
- core/drupal
- core/jquery
- core/drupalSettings
Notice that we have to include drupalSettings
and jquery
from core as dependencies. In Drupal 8, these dependencies aren’t included by default as they were in Drupal 7.
Passing JSON to D3
D3 enables the mapping of an arbitrary dataset to a Document Object Model (DOM), and then allows for manipulation of the document by data-driven changes. D3 can accept text-based data as a plain text file, CSV, or in JSON format. Drupal 8 provides RESTful web services and serialization out-of-the-box to create views that generate JSON data. In this particular use case however, we’re creating our own route and generating the data from form inputs in the controller.
In order for D3 to map all the randomly generated numbers in a bar chart, the data points are returned in a simple array of values as a JsonResponse object. To create a path that only displays JSON, a function in the controller takes the form parameters and returns the correct number of random integers delimited by minimum and maximum values in a JSON callback.
The controller class for generating the JSON needed by the custom JavaScript can be found in the src/Controller
directory and is called BubblesortController.php
:
<?php
namespace Drupal\bubblesort\Controller;
use Drupal\Core\Controller\ControllerBase;
use Symfony\Component\HttpFoundation\JsonResponse;
/**
* Controller for bubblesort routes.
*/
class BubblesortController extends ControllerBase {
/**
* Returns JSON data of form inputs.
*
* @return \Symfony\Component\HttpFoundation\JsonResponse
* A JSON response containing the sorted numbers.
*/
public function bubblesort_json($data) {
if (empty($data)) {
$numbers = numbers_generate();
}
else {
$data_parts = explode('&', $data);
foreach($data_parts as $part) {
$fields[] = explode('=', $part);
}
// Loop through each field and grab values.
foreach($fields as $field) {
if (!empty($field[1])) {
switch ($field[0]) {
case 'numbers_total':
$total = $field[1];;
break;
case integer_min:
$range1 = $field[1];
break;
case integer_max:
$range2 = $field[1];
break;
}
}
}
// Sort the numbers.
$numbers = $this->numbers_generate($total, $range1, $range2, FALSE);
}
// Return a response as JSON.
return new JsonResponse($numbers);
}
/**
* Generates random numbers between delimiters.
*
* @param int $total
* The total number of bars.
* @param int $range1
* The starting number.
* @param int $range2
* The ending number.
* @return array
*/
private function numbers_generate($total = 10, $range1 = 1, $range2 = 100, $sort = FALSE) {
$numbers = range($range1, $range2);
shuffle($numbers);
$numbers = array_slice($numbers, 0, $total);
if ($sort) {
rsort($numbers);
}
return $numbers;
}
}
The functions declared in the controller above are the same callbacks that are used in hook_menu()
of the Drupal 7 version of the module.
Using hook_theme to create a render array
The bar chart for stepping through or playing the sorting animation of the randomly generated numbers needs a place to be appended in the form template. We use hook_theme()
in the module file to generate a render array for the form:
<?php
/**
* @file
* Contains bubblesort.module.
*/
/**
* Implements hook_theme().
*/
function bubblesort_theme($existing, $type, $theme, $path) {
return array(
'bubblesortform' => array(
'template' => 'bubblesort',
'render element' => 'form',
),
);
}
In the Twig template, there is a <div>
element with class "chart" that will get populated with the sorting bars through jquery selectors:
<form___TWIG0___>
___TWIG1___
___TWIG2___
___TWIG3___
<div>
___TWIG4___
</div>
<div>
___TWIG5___
</div>
<div>
___TWIG6___
</div>
<div>
___TWIG7___ ___TWIG8___ ___TWIG9___
</div>
</form>
<div class="chart"></div>
Alternatively, you could create a markup form element and pass the necessary markup into the form.
drupalSettings
The juice of the sorting animation lives in the custom JavaScript file called bubblesort.js
. The functionality is almost identical to the JavaScript file in the Drupal 7 module, save for two instances. First is the syntax for passing a variable via drupalSettings
.
Compare the following line in Drupal 7:
base_path = Drupal.settings.basePath;
In Drupal 8, it changes to:
base_path = drupalSettings.baseUrl;
HTML5 elements
The other minor difference lies in the selector for grabbing all the form inputs. In the Drupal 7 version, the selector looks like:
values = (!empty) ? $('form input:text').serialize() : '';
While in Drupal 8, the selector is:
values = (!empty) ? $('input[type="number"]').serialize() : '';
The reason for this is that in Drupal 8, we’re using a new HTML5 element '#type' => 'number'
for the form elements, while in Drupal 7, the input fields were of type text
.
Our Port to Drupal 8 is Complete!
The meat of the sorting functionality resides in a function called bubblesort_display(data)
in the JavaScript file. This is where the D3 library is used to draw and sort the bar chart based on the data passed into it from the JSON callback. Parsing the JavaScript is beyond the scope of this article, but there are tons of examples and tutorials for how to implement the wild variety of D3 charts and graphs into an application (see Additional Resources below).
So this wraps up the conversion of a Drupal 7 custom module integrated with D3 to Drupal 8. The most challenging parts of such a migration center around the routing mechanism and extending the controller classes to provide the functionality that was defined in a procedural approach using hook_menu()
. The method for passing data to a custom JavaScript file using the D3 library is the same for Drupal 8 as it is for Drupal 7 - using a JSON callback to return the data that can be consumed by D3 methods.
The Drupal 8 version of the bubblesort custom module is also available for download and can be readily installed in a vanilla Drupal 8 site by just enabling it. Here’s a gif of bubblesort in action in Drupal 8: