I hear ya cluckin' big chicken! the nervous rustling of code, tech, and life

Logarithmic Line Charting via Kendo UI

This post revisits an old, Classic ASP application that dynamically generated an Excel spreadsheet with two worksheets; one held the data and the other a logarithmic line chart based on the data. This was accomplished by triggering a series of VBS scripts (from IIS) that automated the spreadsheet creation and prepared it for download. It required MS Office and Excel to be installed on the server and in its heyday, it bedazzled the engineer who came to rely on it. And when it didn't work, it could lock up the web server and deliver great pain and agony to its developer.

Fast forward a few years, (and thanks to .NET, OOXML and HTML5/CSS3), the options are far more numerous and reliable. This post focuses on logarithmic charting via the Telerik Kendo UI Professional suite of widgets and tools. The chart and grid are commercial widgets and are not part of the Kendo UI Core. Kendo UI Core is an open source project currently licensed under Apache v2. However, a free and fully-functional trial of the professional version can be downloaded from Telerik‘s Kendo UI page. To make more sense of the Telerik Kendo UI offerings see a comparison of Core vs. Pro.

See Excel/Kendo Logarithmic Chart Comparison for an Excel vs Kendo screenshot equivalency; i.e. using the exact same data.

Live Demo: Key Points

To provide a date range for the chart, select rows from the grid via mouse drag/drop and/or Shift/click. Alternatively, use the date pickers to define a range. To execute, click the chart icon.

  1. The focus is on the dynamic and responsive qualities of the Kendo UI Chart widget. The grid, and selecting data from it, are secondary and not particularly intuitive or touch friendly. I did this to help focus my testing on the chart.
  2. The data is real oil and gas production data, (from the Anadarko Basin, Texas Panhandle), with fictional well names. Now, before you tune out simply because I’ve done work for a not so popular industry, please read About this blog.
  3. According to the petroleum engineer who schooled me in the virtues of the base 10 logarithmic scale, it is the preferred visual approach to interpreting integrated/multi-axis production data. Here’s a good write-up on log charts.


UI Design Goals

  1. a semi-responsive page menu and Kendo Grid, both fully anchored to a borderless viewport.
  2. a responsive Kendo Window to contain a responsive Kendo Chart and export options. The latter to be left to the reader or perhaps a future post.
  3. the Kendo chart window and inner Kendo chart will open and plot upon button click.

Design Summary

  1. Host the chart widget inside a window widget in modal mode.
  2. Then use the window's open event to create the chart widget based on user selected rows in the grid, or a user entered date range.
  3. Once the user closes the Kendo window, use its close event to properly destroy/dispose of the Kendo chart inside.

Ultimately, I couldn't get the chart to fill the window properly when it was initialized within the window's open event. Once I moved the chart initialization to the chart button click event, it auto-sized and filled the window correctly.

Kendo Widgets

I used the MVC wrappers to initialize the well dropdown list, date pickers and the grid. For the heck of it, I chose to initialize the Kendo Window manually, i.e. without use of its MVC fluent wrapper. I also took this approach with Kendo Chart widget. This seemed to make sense because lazy initialization of the chart is triggered by the user clicking the "Chart" button.

More Detail

Setting up the containers is straight forward.

1
2
3
4
5
6
7
8
9
10
11
<!--
    Kendo Window: holds inner
    chart and export options
 -->
<div id="chartWindow">
    <div id="dailyProdChart">
    </div>
 
    <h3>Export options</h3>
</div>
<!-- End of Kendo Window -->

On DOM ready the Kendo Window is initialized as follows. The window close event is responsible for releasing resources associated with the chart.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// Initialize Kendow window
$("#chartWindow").kendoWindow({
    actions: ["Maximize", "Minimize", "Close"],
    close: function (e) {
        // do garbage collection of Kendo Chart
        var chartObj = $("#dailyProdChart");
        // detach event handlers and jQuery data attributes
        chartObj.data("kendoChart").destroy();
        // remove all child nodes and text within them
        chartObj.empty();
    },
    visible: false,
    draggable: true,
    modal: true,
    resizable: false,
    title: "Daily Production Chart",
    width: "90%",
    height: "85%"
});

When the chart icon is clicked the window is opened and the chart is initialized. The getDataToChartFromGrid() function is responsible for cherry picking data from the grid. 

1
2
3
4
5
6
7
8
// Bind "Chart" button/icon click event
var chartButton = $("#chartButton")
    .bind("click", function() {
        var chartWindow = $("#chartWindow").data("kendoWindow");
        chartWindow.open();
        createDailyProdChart(getDataToChartFromGrid());
        chartWindow.center();
    });

The createDailyProdChart() is the lazy initialization of the chart.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
// Initialize and configure Kendo chart
function createDailyProdChart(charData) {
    //var charData = getDataToChartFromGrid(),
    var rowCount = charData.length,
        well = $("#wellDropDownList").data("kendoDropDownList")
        title = "Daily Production for " + well.text()
            + " (" + kendo.toString(new Date($("#dtStart").val()), "M/d/yy")
            + " - " + kendo.toString(new Date($("#dtEnd").val()), "M/d/yy")
            ", " + charData.length + " day period)";
 
    $("#dailyProdChart").kendoChart({
        dataSource: {
            data:  charData,
            sort: {
                field: "Prod_Date",
                dir: "asc"
            }
        },
        title: {
            text: title,
            font: "bold 1.2em sans-serif"
        },
        legend: {
            position: "bottom",
            labels: {
                padding: { right: 20 }
            }
        },
        seriesDefaults: {
            type: "line",
            style: "normal",
            //style: "smooth",
            width: 1,
            missingValues: "gap",
            markers: {
                visible: true,
                size: 5,
                border: { width: 1 }
            },
        },
        series: [
            {
                field: "MCFD",
                name: "Gas MCF",
                axis: "mcfWaterAndPressures",
                color: "red",
                markers: { type: "square", rotation: 45 }
            },
            {
                field: "PIT_BBLS",
                name: "Water",
                axis: "mcfWaterAndPressures",
                color: "blue",
                markers: { type: "square" }
            },
            {
                field: "Tubing_Pressure",
                name: "Tubing Pressure",
                axis: "mcfWaterAndPressures",
                color: "turquoise",
                markers: { type: "triangle" }
            },
            {
                field: "Casing_Pressure",
                name: "Casing Pressure",
                axis: "mcfWaterAndPressures",
                color: "purple",
                markers: { type: "square" }
            },
            {
                field: "Line_Pressure",
                name: "Line Pressure",
                axis: "mcfWaterAndPressures",
                color: "gold",
                markers: { type: "circle" }
            },
            {
                field: "BOPD",
                name: "Oil",
                axis: "oil",
                color: "green",
                markers: { type: "circle" }
            }
        ],
        axisDefaults: {
            labels: {
                font: "bold 9px sans-serif"
            }
        },
        categoryAxis: [
            {
                labels:  { step:  Math.round(rowCount/9) },
                field: "Prod_Date",
                axisCrossingValue: [0, rowCount],
            }
        ],
        tooltip: {
            visible: true,
            template: "${series.name}: ${value}",
            // usedt to combine values in tooltip (turn off template when using)
            //shared: true,
            format: "N0"
        },
        valueAxis: [
            {
                name: "mcfWaterAndPressures",
                type: "log",
                title: {
                    text: "MCF, Water and Pressures"
                },
                labels: {
                    format: "N0"
                },
                minorGridLines: {
                    visible: true
                }
            },
            {
                name: "oil",
                type: "log",
                title: {
                    text: "Oil"
                },
                labels: {
                    format: "N0"
                },
                minorGridLines: {
                    visible: true
                }
            }
        ]
    });
}

 The following, (along with overall page styles/CSS), is responsible for the responsive magic. Resize the browser window to test drive.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// Respond to changes in viewport
$(window).resize(function () {
    var grid = $("#dailyProdGrid").data("kendoGrid");
    grid.resize();
 
    // if chart window open then respond to window resize
    var chartWindow = $("#chartWindow").data("kendoWindow");
    if ( !chartWindow.element.is(":hidden") ) {
        chartWindow.center();
        var chart = $("#dailyProdChart").data("kendoChart");
        if (chart) {
            chart.resize();
            chart.redraw();
        }
    }
});

The following documentation from Telerik is instrumental in understanding widget resizing and responsive trickery.

That's it!