Kendo UI draggable chart

Swarnendu De April 7, 2014

header

Kendo UI is a comprehensive HTML5/JavaScript framework for modern web and mobile app development. Kendo UI emerges out to be one of the most promising app developing framework. Though it’s a commercial product, it is backed by an excellent and fast support team. With Kendo UI, we can easily create products without worrying about cross-browser compatibility, standards compliance or touch-device support.

Though Kendo UI provide us a lot of facilities but while developing a product we may sometimes need some features which are not supported by the framework. In this blog we will be discussing about one of such feature – “Draggable Chart”. Well in the github project (available for download) we have implemented this draggable feature for “line”, “bar” and “column” charts but we will be explaining only “line” charts in this blog.


Known Issues:

 Since this feature is not supported by Kendo so you may get some issues, like:

  • New chart data not updated – After drag ends, the chart data of that point will not be updated along with the newly changed position. So each time the chart refreshes it will show the old plotted points (original values) and not the changed dragged point.

  • Tooltip and overlay problem – After changing (dragging) a point if you hover a series or that point then it will show the old value in the tooltip and the tooltip position will also be of the original value.

Since we did not give much effort on this feature we were unable to fix these issues. But if you find this blog helpful and want to contribute then please fork the github project and help us complete this feature so that Kendo framework can include this feature in future.

index.html

<html>
    <head>
        <title>Kendo Draggable Chart</title>
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
        <link rel="stylesheet" href="https://www.innofied.com/wp-content/uploads/2014/04/kendo.common.min.css">
   </head>

    <body>

        <div class="kendo-chart"></div>

        <script src="https://www.innofied.com/wp-content/uploads/2014/04/jquery-1.9.1.min.js" type="text/javascript"></script>
        <script src="https://www.innofied.com/wp-content/uploads/2014/04/kendo.all.min.js" type="text/javascript"></script>
        <script src="draggablechart.js" type="text/javascript"></script>

    </body>
</html>
Kendo Draggable Chart
Chart before dragging

For making the chart points draggable we will make use of kendo chart events: “drag” and “dragStart”. For “line” charts, we will make the line intersecting points (circles) draggable. But before starting we need to evaluate the chart boundaries beyond which we will restrict the user to drag the points. Since the chart dimensions are fluid so we preferred to calculate the boundaries every time in the “dragStart” event. Also to retrieve the individual circles and line paths we will set some attributes to them.

dragStart: function(e) {
// Calculate the dimensions only if the target is “circle”
            if(e.originalEvent.target[0].tagName !== 'circle'){
                return;
            }         

            var me = this,
            circle = $('.kendo-chart').find('svg g g circle'), // Get all the line intersecting points
            path = $('.kendo-chart').find('svg g g path'), // Get all the line paths
            pathLength = path.length, // Get the total paths length
            circleLength = circle.length;  // Get the total circles length

            me.index = 0;

	// Calculate the chart boundaries
	 if(!self.chartLimits){
                self.chartLimits = getChartLimits();
            }

            // Set attribute to the line path for retrieving the individual path
            path.each(function() {
                if (!$(this).attr('data-index')) {
                    $(this).attr('data-index', ++me.index);
                }
            });

            // Set attribute to the circles(points) for retrieving the individual circle
            for (var k = 0; k < circleLength; k++) {
                 $(circle[k]).attr({
                    'data-index': (k % pathLength) + 1,
                    'data-bar': Math.ceil((k + 1) / pathLength)
                });
            }
        } 

  // Get the chart area (plot area) limits
    getChartLimits = function(){
        var plotArea = $($('svg path')[2]).attr('d').substr(1),
        plotAreaDim = plotArea.split(" "),

        chartLimits = {
            upperHeightLimit: plotAreaDim[5], // For line/column graphs
            lowerHeightLimit: plotAreaDim[1], // For line/column graphs
            upperWidthLimit: plotAreaDim[2], // For bar graphs
            lowerWidthLimit: plotAreaDim[0] // For bar graphs
        };

        return chartLimits;
    };

Now we have got all our requirements thus we can start with our main draggable functionality.

drag: function(e){
// Drag points(circle) only
            if(e.originalEvent.target[0].tagName === 'circle'){
                movePathWithPoint(e.originalEvent.target[0], e.originalEvent.event.offsetY);
            }
}

movePathWithPoint = function (element, pos) {
     var pointIndex, path, p_array, pathElement ,circle, pathIndex,
        chartLimits = self.chartLimits;

        // Get the target circle element
        circle=  $('circle[data-model-id=' + $(element).attr('data-model-id') + ']');                
        pointIndex = circle.attr('data-bar') - 1;                
        pathIndex = circle.attr('data-index');  

        // Get the line path in which the circle lies              
        pathElement = $('svg g g path[data-index=' + pathIndex + ']');

        // Restrict dragging outside the chart limits 
        if (pos > chartLimits.lowerHeightLimit && pos < chartLimits.upperHeightLimit) {

             // In the pathElement the “d” attribute contains all the coordinates of the points and lines
            path = pathElement.attr('d').substr(1);         

            // Set the line path along with the dragging circle
            p_array = path.split(" ");
            p_array[(pointIndex * 2) + 1] = pos; // Change the path coordinate with the new position of the dragged circle
            path = "M" + p_array.join(" "); 

            pathElement.attr('d', path); // Reset the “d” attribute with the changed coordinates
            circle.attr('cy', pos); // Reset the circles y-coordinate 
        }
 }
Kendo Draggable Chart (1)
Chart after dragging

Summarizing all the code:

draggablechart.js

$(document).ready(function(){
    var self = this;

    $('.kendo-chart').kendoChart({
        title: {
            text: "Revenue By Country"
        },
        legend: {
            position: "bottom"
        },
      series: [{
            name: "United States",
            data: [15.7, 16.7, 20, 23.5, 26.6]
        }, {
            name: "India",
            data: [67.96, 68.93, 75, 74, 78]
        }],
        seriesDefaults: {
            type: "line"
        },
        valueAxis: {
            labels: {
                format: "{0}"
            },
            line: {
                visible: true
            }
        },
        categoryAxis: {
            categories: ['2009', '2010', '2011', '2012'],
            majorGridLines: {
                visible: false
            }
        },
        drag: function(e){
            // Drag points(circle) only
            if(e.originalEvent.target[0].tagName === 'circle'){
                movePathWithPoint(e.originalEvent.target[0], e.originalEvent.event.offsetY);
            }
        },

        dragStart: function(e) {
            // Calculate the dimensions only if the target is “circle”
            if(e.originalEvent.target[0].tagName !== 'circle'){
                return;
            }         

            var me = this,
            circle = $('.kendo-chart').find('svg g g circle'), // Get all the line intersecting points
            path = $('.kendo-chart').find('svg g g path'), // Get all the line paths
            pathLength = path.length, // Get the total paths length
            circleLength = circle.length;  // Get the total circles length

            me.index = 0;

            // Calculate the chart boundaries
            if(!self.chartLimits){
                self.chartLimits = getChartLimits();
            }

            // Set attribute to the line path for retrieving the individual path
            path.each(function() {
                if (!$(this).attr('data-index')) {
                    $(this).attr('data-index', ++me.index);
                }
            });

            // Set attribute to the circles(points) for retrieving the individual circle
            for (var k = 0; k < circleLength; k++) {
                $(circle[k]).attr({
                    'data-index': (k % pathLength) + 1,
                    'data-bar': Math.ceil((k + 1) / pathLength)
                });
            }

        }

    });

    // Get the chart area (plot area) limits
    var getChartLimits = function(){
        var plotArea = $($('svg path')[2]).attr('d').substr(1),
        plotAreaDim = plotArea.split(" "),

        chartLimits = {
            upperHeightLimit: plotAreaDim[5], // For line/column graphs
            lowerHeightLimit: plotAreaDim[1], // For line/column graphs
            upperWidthLimit: plotAreaDim[2], // For bar graphs
            lowerWidthLimit: plotAreaDim[0] // For bar graphs
        };

        return chartLimits;
    };

    // Draggable functionality     
    var movePathWithPoint = function (element, pos) {
        var pointIndex, path, p_array, pathElement ,circle, pathIndex,
        chartLimits = self.chartLimits;

        // Get the target circle element
        circle=  $('circle[data-model-id=' + $(element).attr('data-model-id') + ']');                
        pointIndex = circle.attr('data-bar') - 1;                
        pathIndex = circle.attr('data-index');  

        // Get the line path in which the circle lies              
        pathElement = $('svg g g path[data-index=' + pathIndex + ']');

        // Restrict dragging outside the chart limits 
        if (pos > chartLimits.lowerHeightLimit && pos < chartLimits.upperHeightLimit) {

            // In the pathElement the “d” attribute contains all the coordinates of the points and lines
            path = pathElement.attr('d').substr(1);         

            // Set the line path along with the dragging circle
            p_array = path.split(" ");
            p_array[(pointIndex * 2) + 1] = pos; // Change the path coordinate with the new position of the dragged circle

            path = "M" + p_array.join(" "); 

            pathElement.attr('d', path); // Reset the “d” attribute with the changed coordinates
            circle.attr('cy', pos); // Reset the circles y-coordinate 
        }
    }
});

This is it. Hope this blog helps you to some extent and you can use this functionality in your kendo apps. If you get any issue implementing this, do not hesitate to add a comment here.