Telerik blogs
sotw-survey-870x220

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:

SOTW Survey 2018 + kendoka-sitting-tiny

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. 😀

Visualizing Opinion Statements

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.

Let's Add Some Color

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:

Calculating and Displaying Averages

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.

Let's Add Sparklines For More Zing

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.

User Interactions

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:

Visualizing Yes/No Questions

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:

Go Have Some Fun!

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!


About the Author

John Bristowe

John Bristowe is a member of the Developer Relations team at Progress. He specialises in web and mobile app development.

Comments

Comments are disabled in preview mode.