Kendo UI provides rich widgets to visualize data in meaningful ways. In this post, John Bristowe shows you how you can creatively display your data.
Recently, the folks at dev.to conducted its State of the Web Survey, which was completed by 1899 respondents. The results were shared online and a call to arms was put forth: State Of The Web Data - Call For Analysis! I took the challenge to visualize this data using Kendo UI:
In this blog post, I'll show you how I built this page. In particular, I'll focus on two sections of the survey that I found to be the most interesting; a set of opinion statements and a series of yes/no questions. Both of these were built using Kendo UI, which provides the building blocks for creating rich data visualizations. At the end, I think you'll agree that it's fun to paint outside the lines. 😀
The State of the Web survey asked developers to provide opinions on a series of statements. For example:
Web innovation is outpacing native
Respondents were asked to provide their opinions on these statements by scoring them from 0
(disagree) to 10
(agree).
In total, 18 statements were scored with 11 different values for each (0-10, inclusive; excluding blank scores). Given the breadth of score values across a large number of statements, I felt the Grid
was the best control to use in order to display this data in a meaningful way.
To begin, I tabulated these opinions into the following data structure:
// opinionData.json
[
{
"text": "Web innovation is outpacing native",
"a0": 32,
"a1": 10,
"a2": 29,
"a3": 39,
"a4": 76,
"a5": 286,
"a6": 253,
"a7": 401,
"a8": 361,
"a9": 131,
"a10": 209,
"Blank": 72
},
// other opinions and vote totals here...
]
Each opinion (text
) is represented in an array with the total votes for each answer (a0
equals 0
or disagree, a10
equals 10
or agree). Afterward, I bound this data to a Grid
with an element ID, opinionGrid
:
$("#opinionGrid").kendoGrid({
columns: [
{ field: "text", title: "Opinion (0: Disagree, 10: Agree)" },
{ field: "a0", title: "0" },
{ field: "a1", title: "1" },
{ field: "a2", title: "2" },
{ field: "a3", title: "3" },
{ field: "a4", title: "4" },
{ field: "a5", title: "5" },
{ field: "a6", title: "6" },
{ field: "a7", title: "7" },
{ field: "a8", title: "8" },
{ field: "a9", title: "9" },
{ field: "a10", title: "10" }
],
dataSource: { data: opinionData }
});
This configuration generates an informative but rather bland-looking Grid
:
The Grid
provides a number of built-in features like paging, sorting, and filtering. Enabling sorting is easy; simply set the sortable
property to true
and your users will be able to perform sort operations by clicking on the column header. Sorting by column, 10
– indicating the most agreement with an opinion – we can see that many developers felt most strongly against online advertisements:
Unfortunately, sorting data is this manner doesn't tell the whole story. Other questions remain. For example, where are opinions clustered? Do the majority of developers agree or disagree with an opinion? Or, are they indifferent? The data – as it's presented here – doesn't hold much semantic weight.
The good news is that we can improve this situation by adding color to denote the value of a cell. This is accomplished by defining a row template for the Grid
. This is a useful extensibility mechanism of the Grid
that allows us to control the markup that's generated for each row. We can combine this feature with chroma.js to generate a scale of colors based on the value of each cell:
import chroma from "chroma-js";
const voteBackgroundColor = chroma.scale(["fff","66f"]).domain([min,max]);
const voteColumnColor = function (votes) {
return voteBackgroundColor(votes).luminance() > 0.5 ?
"black" :
"white";
};
$("#opinionGrid").kendoGrid({
// ...
rowTemplate: function (e, data) {
var template = "<tr data-uid=\"" + kendo.guid() + "\">" +
"<td>" + e.text + "</td>" + // opinion column
"<td style=\"background-color:" + voteBackgroundColor(e.a0).hex() + ";" +
"color:" + voteColumnColor(e.a0) + "\">" + e.a0 + "</td>";
// output other columns (e.a1, e.a2, etc.)
template += "</tr>";
return template;
}
});
I've used chroma.js to define a color scale function called voteBackgroundColor
based on the minimum (min
) and maximum (max
) values in the Grid
. (These are calculated elsewhere.) Low values are be represented by a lighter background (white) while high values are be represented by a darker background (blue). These values are generated through the hex()
API and applied through the background-color
style. The luminance()
API of chroma.js is used for the voteColumnColor
function to determine the color
style for the cell. Brighter colors use black while dimmer colors use white. This provides a level of contrast for a better user experience.
As you can see, adding the color provides more insight into where scores are clustered around particular opinions. A quick glance at this data reveals some clustering for opinions like “I am satisfied with the state of A/B test tooling for the web” and “I dislike ads on the web.” If we ignore the data analysis and look more closely, we get a better sense of the overall appearance after using the luminance of the background color to determine the font color:
This is accomplished without impacting the functionality of the Grid
. For example, we can still sort the data:
Let's take this one step further by adding a column for displaying an average score for each row. This will allow us to determine the most/least supported opinions. If I calculate these values on-the-fly within the rowTemplate
function, the Grid
would not be able to sort them. That's because operations (like sorting) are conducted through the bound DataSource
; not the control to which it's bound (i.e.Grid
). Therefore, to preserve the sorting capabilities of the Grid
, I must persist the average score in the bound data source:
// scores are 0-10 (inclusive)
function calculateAverages(opinions) {
for (var i = 0; i < opinions.length; i++) {
var avg = 0;
for (var j = 0; j < 11; j++) {
avg += j * opinions[i]["a" + j];
}
avg = avg / total;
opinions[i].avg = avg;
}
return opinions;
}
Once the average score has been added to the bound data source, we can display it in the Grid
like so:
$("#opinionGrid").kendoGrid({
columns: [
{ field: "avg", title: "Avg" }
// other columns here...
]
});
Here is the Grid
sorted by an average column:
Wow. Developers really don't like ads on the web. 🤣 They are also optimistic about the future of PWAs and feel that the web is improving for end-users. Conversely, they appear to disagree with the opinions, “the desktop web is typically slow and sluggish” and “I am satisfied with the state of A/B test tooling for the web”.
We can add some punch to the Grid
by using the approach we used previously to color each cell based on its value. Here, we'll define another color scale function called fAvg
based on the minimum (minAvg
) and maximum (minAvg
) average scores:
const fAvg = chroma.scale(["fff","999"]).domain([minAvg,minAvg]);
$("#opinionGrid").kendoGrid({
// ...
rowTemplate: function (e, data) {
// ...
var avgColumnColor = fAvg(e.avg).luminance() > 0.5 ? "black" : "white";
template += "<td style=\"background-color:" + fAvg(e.avg).hex() + ";color:" + avgColumnColor + "\">" + e.a0 + "</td>";
// ...
}
});
Here is the Grid
with this change:
The data visualization capabilities support both SVG and the HTML5 <canvas>
element.
A Sparkline
is a small chart that you can embed into other controls like the Grid
. It is super-economical in terms of screen real estate because it packs a lot of information into a tiny space.
Let's add another column to display a Sparkline
for each row. We'll have to get a little creative to do this because the Grid
has a rowTemplate
defined. I'll create a function that will output the markup that's required to create a Sparkline
in a declarative fashion (via data-*
attributes):
function getSparkline(data) {
return "<td><div class=\"sparkline\"" data-chart-area=\"{background:'transparent'}\" data-role=\"sparkline\" data-series=\"[{type:'column',data:[" + data + "]}]\" data-series-defaults=\"{gap:0.1}\" data-theme=\"material\"></div></td>";
}
I'll add a handler for the dataBound
event that fires for the Grid
to initialize each Sparkline
and generate the Sparkline
through the rowTemplate
function:
$("#opinionGrid").kendoGrid({
// ...
columns: [
// other columns here...
{ } // add a column for the sparkline
],
dataBound: function (e) {
kendo.bind(".sparkline");
}
rowTemplate: function (e, data) {
// ...
template += getSparkline(opinions);
}
});
Here's the result:
Despite its size, a Sparkline
preserves the fidelity of the data by rendering content as SVG. Here's an example at a 500% zoom level:
The Sparkline
can also render out to an HTML5 <canvas>
element as well.
By default, rows are highlighted in the Grid
when a user hovers over them. Since we've applied styles in each of the cells for the background color, this overrides the default behaviour. For example, if we hover over a row, the row's style is overridden by the ones we added earlier in the rowTemplate
function:
Let's fix this behaviour with a style rule that highlights each row:
#opinionGrid tbody > tr:hover > td {
background-color: #ffa !important;
color: black !important;
}
I'm not a fan of using !important
but it's necessary in this case to override the color styles applied earlier. With this change, individual rows will be highlighted when the mouse hovers over them:
Another section of the survey asked developers to provide yes/no answers to a series of questions. For example:
Does your team/project support IE 10 and under?
In total, 9 questions were answered (excluding blank scores) as yes
or no
with associated totals. A suitable approach for visualizing this data is to use the Grid
in combination with a Sparkline
to create a yes/no chart:
I was surprised to see that not many developers use WebP-formatted images in their web apps. I would have speculated that more developers are using the format these days. I suppose new image formats can take a lot of time to be adopted.
The configuration used to create this visualization is fairly simple:
$("#yesNoGrid").kendoGrid({
columns: [
{ field: "Question" },
{ field: "No", headerAttributes: { style: "text-align:right;" } },
{ field: "Yes" }
],
dataBound: function (e) {
kendo.bind(".yesNoSparkline");
},
dataSource: { data: yesNoData, sort: { field: "No", dir: "desc" } },
rowTemplate: function (e, data) {
return "<tr data-uid=\"" + kendo.guid() + "\">" +
"<td>" + e.Question + "</td>" +
getYesNoColumn("No", e.No, "#f66") +
getYesNoColumn("Yes", e.Yes, "#00b312") +
"</tr>";
},
scrollable: false,
sortable: true
});
Here, we're using the same technique used to create the Grid
in the previous section by defining a rowTemplate
function. This uses a function called getYesNoColumn
which is a helper function for generating the Sparkline
:
function getYesNoColumn(vote, voteCount, color) {
var votePosition = voteCount > 400 ? "insideEnd" : "outsideEnd";
var voteLabelColor = voteCount > 400 ? "#fff" : "#000";
var reverse = vote == "No" ? ",reverse:true" : "";
var textAlignment = vote == "No" ? " style=\"text-align:right;\"" : "";
return "<td" + textAlignment + "><div class=\"yesNoSparkline\" data-chart-area=\"{background:'transparent',margin:0}\" data-value-axis=\"{max:1899" + reverse + "}\" data-role=\"sparkline\" data-plot-area=\"{background:'transparent',margin:0}\" data-series=\"[{color:'" + color + "',data:[" + voteCount + "],labels:{color:'" + voteLabelColor + "',position:'" + votePosition + "',visible:true},stack:false,type:'bar'}]\" data-series-defaults=\"{gap:0,margin:0,spacing:0}\" data-theme=\"flat\" data-tooltip=\"{visible:false,shared:false}\"></div></td>";
}
This function is used to determine where to place the labels for each Sparkline
widget that's generated in the Grid
. Notice how the label is placed on the outside of the Sparkline
if its value is too small to render inside it:
Kendo UI provides the building blocks for creating rich visualizations of data. However, it doesn't limit you from getting creative. In fact, it's fun to paint outside the lines sometimes. As you've seen in this blog post, there's a lot of interesting ways to visualize data with Kendo UI.
You can check out the full survey data visualization I built with Kendo UI up on StackBlitz: SOTW Survey 2018. In the meantime, don't be afraid to have some fun with using Kendo UI for your data visualization projects!
John Bristowe is a member of the Developer Relations team at Progress. He specialises in web and mobile app development.