Howto create one touch Line chart adjusting that's usable on touch devices

Share RGraph:  

Summary
This HOWTO demonstrates how to go about creating an adjustable Line chart that doesn't use the built in adjusting functionality.

The Line chart adjusting can be very useful - but it's not terribly functional on touch based devices (eg mobiles and tablets). This is because of the requirement to drag a point on the Line to its new location - and dragging isn't terribly intuitive or feasible in these environments.

But the Line chart can still be adjusted in these environments as this example shows you. Custom coding is needed - but as its all provided to you here it's just a matter of copying and pasting! There's also a demo that you can examine that provides you with an example of it in action. And you can find that demo in the demos folder here. But first lets have a look at the code that goes into making this page.

1. The code that creates the chart

<script>
    // The data for the chart
    var data = [5,14,2,4,1,8,6,5,9,4];

    // Create and configure the chart
    var line = new RGraph.Line({
        id: 'cvs',
        data: data,
        options: {
            ymax: 20,
            hmargin: 10,
            tickmarks: 'circle',
            labels: ['Fred', 'John', 'Kev', 'Lou', 'Pete','Mark','Neil','Indra','Lewis','Bob'],
            axisColor: '#aaa',
            numyticks: 5,
            noxaxis: true,
            backgroundGridVlines: false,
            backgroundGridBorder: false,
            labelsAbove: true,
            labelsAboveBorder: false,
            labelsAboveDecimals: 1,
            textAccessible: false
        }
    }).draw();
</script>

So this is the code that goes into making the chart. Nothing should particularly strike you as being massively different but a few things are worthy of note:

  1. The ymax property has been set so that when you adjust the line the scale definitely won't change. If you didn't set this and the points were moved down below a certain level the scale would change (eg from a maximum value of 20 to a maximum value of 15 - then ten - then 5 etc).
  2. The tickmarks property has been set to be circle. This is to make it easier for the user to see where the 'grab points' are so they can move them up or down.

2. The canvas mousedown event

<script>
    //
    // This is the function that handles adjusting the line when
    //the chart is touched.
    //
    line.canvas.onmousedown = function(e)
    {
        var obj      = e.target.__object__,
            newvalue = obj.getValue(e),
            mouseXY  = RGraph.getMouseXY(e),
            mouseX   = mouseXY[0],
            mouseY   = mouseXY[1],
            coords   = obj.coords2[0];

        // Determine the closest point to the touch/click This function is defined below
        var index = closest({
            coords: coords,
            mousex: mouseX,
            tolerance: 10 // Pixels, default is 10
        });




        if (index >= 0) {
            data[index] = newvalue;
            
            grow({
                object: obj,
                index: index,
                value: newvalue
            })
        }
    };
</script>

This is the canvas mousedown event that's run whenever the mouse is clicked on the canvas. Here's a description: It gets the mouse coordinates of the click and using those works out the closest Line chart point based on the X position (this is what the closest() function does). The tolerance allows a certain amount of leeway either side of the point so that you don't have to be exact when touching the chart. Remember that touching with a finger is far less accurate than clicking with a mouse. If there is an index (of a point) returned - not null - then that point is animated to its new position by the grow() function.

3. The closest() function

<script>
    //
    // Finds the closest point to the given mouseX coordinate. It allows a
    // tolerance of 10 (or so) pixels.
    //
    //  @param object opt An object consisting of;
    //                     o coords The coordinates of the points
    //                     o mousex The mouseX coordinate
    //                     o tolerance The number of pixels leeway
    //                       that is allowed. Default is 10
    //
    function closest (opt)
    {
        var coords    = opt.coords,
            mouseX    = opt.mousex,
            tolerance = (typeof opt.tolerance === 'number' ? opt.tolerance : 10),
            point     = null;

        // Loop through the coordinates looking for the closest
        // (going by X coordinate)
        for (var i=0,distance = null; i<coords.length; ++i) {
            if (mouseX > coords[i][0] - tolerance && mouseX < coords[i][0] + tolerance) {
                point = i;
            }
        }
        
        return point;
    }
</script>

This is the function that's used to determine the closest point to the mouse cursor - if any. It simply loops through the objects coordinates and compares the X coordinate to the mouse X position. It uses the tolerance (which by default is 10) to allow a little leeway each side. If it finds a point it returns the index of it.

4. The grow() function

<script>
    //
    // The animation function that makes the point grow to
    // its new position. 
    //
    function grow (opt)
    {
        var obj   = opt.object,
            idx   = opt.index,
            value = opt.value;
            
        
        // Determine the original value or the point thats being adjusted
        var original_value = line.original_data[0][idx];


        var frames = 15,
            delay  = 16.666; // milliseconds

        for (var i=0; i<frames; i++) {
            (function (i)
            {
                setTimeout(function ()
                {
                    line.original_data[0][idx] = ((value - original_value) * (i + 1) / frames) + original_value;
                    
                    // Update this so that the above labels are correctly updated
                    line.data_arr = RGraph.arrayLinearize(line.original_data);
                    
                    RGraph.redraw();
                }, delay * i);
            })(i)
        }
    }
</script>

This is the animation function that makes the point move to the desired new position. Each frame is done by updating the value in the chart and then redrawing the chart. A line that's worthy of note is this:

line.data_arr = RGraph.arrayLinearize(line.original_data);
This is not normally necessary however is if you use the labelsAbove option - which this chart does. This option uses the data_arr property so if it wasn't updated then the values displayed would be incorrect (this variable is intially set in the constructor - which isn't being repeatedly called on every frame).

All of the code put together

Here's the code put together. You can see an example by looking at the demo linked below (which, as of version 4.63, is also included in the download). If you need the demo and are using a prior version of RGraph - simply view-source: on this page and copy it to your work environment.

<script>
    // The data for the chart
    var data = [5,14,2,4,1,8,6,5,9,4];

    // Create and configure the chart
    var line = new RGraph.Line({
        id: 'cvs',
        data: data,
        options: {
            ymax: 20,
            hmargin: 10,
            tickmarks: 'circle',
            labels: ['Fred', 'John', 'Kev', 'Lou', 'Pete','Mark','Neil','Indra','Lewis','Bob'],
            axisColor: '#aaa',
            numyticks: 5,
            noxaxis: true,
            backgroundGridVlines: false,
            backgroundGridBorder: false,
            labelsAbove: true,
            labelsAboveBorder: false,
            labelsAboveDecimals: 1,
            textAccessible: false
        }
    }).draw();




    //
    // This is the function that handles adjusting the line when
    //the chart is touched.
    //
    line.canvas.onmousedown = function(e)
    {
        var obj      = e.target.__object__,
            newvalue = obj.getValue(e),
            mouseXY  = RGraph.getMouseXY(e),
            mouseX   = mouseXY[0],
            mouseY   = mouseXY[1],
            coords   = obj.coords2[0];

        // Determine the closest point to the touch/click
        var index = closest({
            coords: coords,
            mousex: mouseX,
            tolerance: 10 // Pixels, default is 10
        });




        if (index >= 0) {
            data[index] = newvalue;
            
            grow({
                object: obj,
                index: index,
                value: newvalue
            })
        }
    };




    //
    // Finds the closest point to the given mouseX coordinate. It allows a
    // tolerance of 10 (or so) pixels.
    //
    //  @param object opt An object consisting of;
    //                     o coords The coordinates of the points
    //                     o mousex The mouseX coordinate
    //                     o tolerance The number of pixels leeway
    //                       that is allowed. Default is 10
    //
    function closest (opt)
    {
        var coords    = opt.coords,
            mouseX    = opt.mousex,
            tolerance = (typeof opt.tolerance === 'number' ? opt.tolerance : 10),
            point     = null;

        // Loop through the coordinates looking for the closest
        // (going by X coordinate)
        for (var i=0,distance = null; i<coords.length; ++i) {
            if (mouseX > coords[i][0] - tolerance && mouseX < coords[i][0] + tolerance) {
                point = i;
            }
        }
        
        return point;
    }




    //
    // The animation function that makes the point grow to
    // its new position. 
    //
    function grow (opt)
    {
        var obj   = opt.object,
            idx   = opt.index,
            value = opt.value;
            
        
        // Determine the original value or the point thats being adjusted
        var original_value = line.original_data[0][idx];


        var frames = 15,
            delay  = 16.666;

        for (var i=0; i<frames; i++) {
            (function (i)
            {
                setTimeout(function ()
                {
                    line.original_data[0][idx] = ((value - original_value) * (i + 1) / frames) + original_value;
                    
                    // Update this so that the above labels are correctly updated
                    line.data_arr = RGraph.arrayLinearize(line.original_data);
                    
                    RGraph.redraw();
                }, delay * i);
            })(i)
        }
    }
</script>