Chapter 4. Creating a Color Picker with D3

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.

Creating a slider control

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.

Creating a slider control

The final slider component

The drag behavior

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.

Creating the 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:

Creating the slider

The slider appended to an SVG group

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.

Using the slider

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:

Using the slider

The fill color of the rectangle is controlled by the slider

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset