In this chapter, we will implement a slider and a color picker using D3. We will use the reusable chart pattern to create the slider and the color picker. We will also learn how to compose reusable charts in order to create more complex components.
A slider is a control that allows a user to select a value within a given interval without having to type it. It has a handle that can be displaced over a base line; the position of the handler determines the selected value. The value is then used to update other components of the page. In this section, we will create a slider with D3 using the reusable chart pattern. We will include an API to change its visual attributes and modify other elements when the slider value changes. Note that in HTML5, we can create an input
element of type range
, which will be displayed as a slider with configurable minimum and maximum steps and values. The type color
is also available, which allows us to use the native color picker. Native controls include accessibility features and using the keyboard to control the slider. More details on the input element can be found in https://developer.mozilla.org/en/docs/Web/HTML/Element/Input. To follow the examples of this section, open the chapter04/01-slider.html
file.
We will review how to use the drag behavior with a simple example. We will begin by creating the svg element and put a gray circle in the center:
// Width and height of the figure. var width = 600, height = 150; // Create the svg element. var svg = d3.select('#chart').append('svg') .attr('width', width) .attr('height', height); // Append a grey circle in the middle. var circle = svg.append('circle') .attr('cx', width / 2) .attr('cy', height / 2) .attr('r', 30) .attr('fill', '#555'),
This will create the svg and circle elements, but the circle can't be moved yet. D3 allows us to detect gestures on an element by using behaviors, which are functions that create event listeners for gesture events on a container element. To detect the drag gesture, we can use the drag behavior. The drag behavior detects dragging events of three types, dragstart
, drag
, and dragend
. We can create and configure the drag behavior by adding event listeners for one or more drag events; in our case, we will add a listener for the drag event as follows:
// Create and configure a drag behavior.
var drag = d3.behavior.drag().on('drag', dragListener);
For more details on the D3 drag behavior, consult the D3 wiki page on this subject at https://github.com/mbostock/d3/wiki/Drag-Behavior. To add the drag behavior to the circle, we can call the drag
function, passing the circle selection to it or using the call
method as follows:
// Add dragging handling to the circle. circle.call(drag);
When the user drags the circle, the dragListener
function will be invoked. The dragListener
function receives the data item bound to the circle (if any), with the this
context set to the container element, in our case, the circle element. In the dragListener
function, we will update the position of the circle. Note that the cx
and cy
attributes are returned as strings, and prepending a plus sign will cast these values to numbers. Refer to the following code:
// Moves the circle on drag. function dragListener(d) { // Get the current position of the circle var cx = +d3.select(this).attr('cx'), cy = +d3.select(this).attr('cy'), // Set the new position of the circle. d3.select(this) .attr('cx', cx + d3.event.dx) .attr('cy', cy + d3.event.dy); }
The dragListener
function updates the cx
and cy
attributes of the circle, but it can change other attributes as well. It can even change properties of other elements. In the next section, we will use the drag behavior to create a simple SVG slider.
The slider component will have a configurable width, domain, and a listener function to be called on the slide. To use the slider, attach it to an svg group. As the group can be translated, rotated, and scaled, the same slider can be displayed horizontally, vertically, or even diagonally anywhere inside the SVG element. We will implement the slider using the reusable chart pattern:
function sliderControl() { // Slider Attributes... // Charting function. function chart(selection) { selection.each(function(data) { // Create the slider elements... }); } // Accessor Methods... return chart; }
We will add attributes for width
and domain
, as well as their corresponding accessor methods, chart.width
and chart.domain
. Remember that the accessor methods should return the current value if they are invoked without arguments and return the chart
function if a value is passed as an argument:
function chart(selection) { selection.each(function(data) { // Select the container group. var group = d3.select(this); // Create the slider content... }); }
We will assume that the slider is created within an svg group, but we could have detected the type of the container element and handle each case. If it were a div, for instance, we could append an svg element and then append a group to it. We will work with the group to keep things simple. We will create the base line using the svg line element:
// Add a line covering the complete width. group.selectAll('line') .data([data]) .enter().append('line') .call(chart.initLine);
We encapsulate the creation of the line in the chart.initLine
method. This function will receive a selection that contains the created line and sets its position and other attributes:
// Set the initial attributes of the line. chart.initLine = function(selection) { selection .attr('x1', 2) .attr('x2', width - 4) .attr('stroke', '#777) .attr('stroke-width', 4) .attr('stroke-linecap', 'round'), };
We set the x1
and x2
coordinates of the line. The default value for the coordinates is zero, so we don't need to define the y1
and y2
coordinates. The stroke-linecap
attribute will make the ends of the line rounded, but we will need to adjust the x1
and x2
attributes to show the rounded corners. With a stroke width of 4 pixels, the radius of the corner will be 2 pixels, which will be added in each edge of the line. We will create a circle in the group in the same way:
// Append a circle as handler. var handle = group.selectAll('circle') .data([data]) .enter().append('circle') .call(chart.initHandle);
The initHandle
method will set the radius, fill color, stroke, and position of the circle. The complete code of the function is available in the example file. We will create a scale to map the value of the slider to the position of the circle:
// Set the position scale. var posScale = d3.scale.linear() .domain(domain) .range([0, width]);
We correct the position of the circle, so its position represents the initial value of the slider:
// Set the position of the circle. handle .attr('cx', function(d) { return posScale(d); });
We have created the slider base line and handler, but the handle can't be moved yet. We need to add the drag behavior to the circle:
// Create and configure the drag behavior. var drag = d3.behavior.drag().on('drag', moveHandler); // Adds the drag behavior to the handler. handler.call(drag);
The moveHandler
listener will update only the horizontal position of the circle, keeping the circle within the slider limits. We need to bind the value that we are selecting to the handle (the circle), but the cx
attribute will give us the position of the handle in pixels. We will use the invert
method to compute the selected value and rebind this value to the circle so that it's available in the caller function:
function moveHandle(d) { // Compute the future position of the handler var cx = +d3.select(this).attr('cx') + d3.event.dx; // Update the position if it's within its valid range. if ((0 < cx) && (cx < width)) { // Compute the new value and rebind the data d3.select(this).data([posScale.invert(cx)]) .attr('cx', cx); } }
To use the slider, we will append an SVG figure to the container div and set its width
and height
:
// Figure properties. var width = 600, height = 60, margin = 20; // Create the svg element and set its dimensions. var svg = d3.select('#chart').append('svg') .attr('width', width + 2 * margin) .attr('height', height + 2 * margin);
We can now create the slider
function, setting its width
and domain
:
// Valid range and initial value. var value = 70, domain = [0, 100]; // Create and configure the slider control. var slider = sliderControl().width(width).domain(domain);
We create a selection for the container group, bind the data array that contains the initial value, and append the group on enter
. We also translate the group to the location where we want the slider and invoke the slider
function using the call
method:
var gSlider = svg.selectAll('g') .data([value]) .enter().append('g') .attr('transform', 'translate(' + [margin, height / 2] + ')') .call(slider);
We have translated the container group to have a margin, and we have centered it vertically. The slider is now functional, but it doesn't update other components or communicate changes in its value. Refer to the following screenshot:
We will add a user-configurable function that will be invoked when the user moves the handler, along with its corresponding accessor function, so that the user can define what should happen when the slider is changed:
function sliderControl() { // Slider attributes... // Default slider callback. var onSlide = function(selection) { }; // Charting function... function chart() {...} // Accessor Methods // Slide callback function chart.onSlide = function(onSlideFunction) { if (!arguments.length) { return onSlide; } onSlide = onSlideFunction; return chart; }; return chart; }
The onSlide
function will be called on the drag listener function, passing the handler selection as an argument. This way, the value of the slider will be passed to the onSlide
function as the bound data item of the selection argument:
function moveHandler(d) {
// Compute the new position of the handler
var cx = +d3.select(this).attr('cx') + d3.event.dx;
// Update the position within its valid range.
if ((0 < cx) && (cx < width)) {
// Compute the new value and rebind the data
d3.select(this).data([posScale.invert(cx)])
.attr('cx', cx)
.call(onSlide);
}
}
Remember that the onSlide
function should receive a selection, and through the selection, it should receive the value of the slider. We will use the onSlide
function to change the color of a rectangle.
We use the slider to change the color of a rectangle. We begin by creating the svg element, setting its width
, height
, and margin
:
// Create the svg element var svg = d3.select('#chart').append('svg') .attr('width', width + 2 * margin) .attr('height', height + 3 * margin);
We create a linear color scale; its range will be the colors yellow and red. The domain of the scale will be the same as that in the slider:
// Create a color scale with the same range that the slider var cScale = d3.scale.linear() .domain(domain) .range(['#edd400', '#a40000']);
We add a rectangle in the svg, reserving some space in its upper side to put the slider on top. We also set its width, height, and fill color:
// Add a background to the svg element. var rectangle = svg.append('rect') .attr('x', margin) .attr('y', 2 * margin) .attr('width', width) .attr('height', height) .attr('fill', cScale(value));
We create the slider control and configure its attributes. The onSlide
function will change the rectangle fill color using the previously defined scale:
// Create and configure the slider control. var slider = sliderControl() .domain(domain) .width(width) .onSlide(function(selection) { selection.each(function(d) { rectangle.attr('fill', cScale(d)); }); });
Finally, we append a group to contain the slider and translate it to put it above the rectangle. We invoke the slider
function using the call
method:
// Create a group to hold the slider and add the slider to it. var gSlider = svg.selectAll('g').data([value]) .enter().append('g') .attr('transform', 'translate(' + [margin, margin] + ')') .call(slider);
Refer to the following screenshot: