Using HSL in CSS to shift though traffic light colors for a status inicator

I’ve been rewriting my dashboard project recently and I wanted to find a way to programatically shift through traffic light colors to inicate the status of code coverage coming out of sonar.

CSS3 introduced HSL(Hue-Saturation-Lightness). When you shift hue from 0-120 it goes from red to orange to yellow to green. Perfect for an indicator.

So the simplest code can be

1
2
3
4
var elem = document.getElementById('color-me');
var percentage = 100;
var hue = percentage * 1.2;
elem.setAttribute("style","background-color:hsl(" + hue + ",75%,50%)");

jsfiddle

I’m multipling by 1.2 to make 100% result in a hue of 120 which is green, 0% will still be 0 which is red.

I really want 50% and below to be solid red and then start shifting to green as coverage goes up. I can get that by subtracting 50 from the percentage. I also need to multiple by 2.4 and take the max of that value and 0.

1
2
3
4
var elem = document.getElementById('color-me');
var percentage = 100;
var hue = Math.max((percentage - 50) * 2.4, 0);
elem.setAttribute("style","background-color:hsl(" + hue + ",75%,50%)");

I also want to see the colors change less gradually. I’d like to see a jump in color as an increment of 10 is crossed.

1
2
3
4
var elem = document.getElementById('color-me');
var percentage = 100;
var hue = Math.round((Math.max((percentage - 50) * 2.4, 0) / 10)) * 10;
elem.setAttribute("style","background-color:hsl(" + hue + ",75%,50%)");

jsfiddle with a slider to demonstrate

Removing if statements in javascript

I’ve been playing around with some JavaScript to make a build dashboard that can pull data from Jenkins or Bamboo. I’ve been using testem w/ jasmine to TDD my way through this experiment. I stumbled across this refactoring twice. I don’t remember where I picked this technique up from, but I thought it was worth writing up in anyway. The premise is simple. The type of build (bamboo or jenkins) will continue to creep up and cause me to write if statements that look like this.

1
2
3
4
5
if(type == jenkins){
  // do jenkins stuff
} else if (type == bamboo){
  // do bamboo stuff
}

The first time this came up it was to because the json urls for the two build systems are different. I wanted my users to be able to just go to a normal build URL and copy and paste that into the config instead of figuring out what the RESTful url would be. The second time I came across it was so that I could create a single object from either response that would have the same attributes. This would let me do something like data.total_tests regardless of if the build data came from jenkins (totalCount) or bamboo (successfulTestCount + failedTestCount). So let’s get into the refactoring.

First we start with the initial test for bamboo:

1
2
3
4
5
6
7
8
9
10
11
12
13
it("should parse the bamboo data into a build data object", function(){
  
  // use a spy so we don't use a live ajax request
  spyOn($, "ajax").andCallFake(function(options) {
      options.success(bamboo_response);
  });

  var bamboo_build = dashboard.groups[0].builds[1];
  bamboo_build.refresh();

  expect(bamboo_build.data.failed_tests).toEqual(6);
  expect(bamboo_build.data.total_tests).toEqual(1053);
});

This causes us to write a simple solution that parses just the bamboo data and returns a new build agnostic data object:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
build : {
  refresh : function() {
      var this_build = this;
      $.ajax({
          ...
          success: function(json) {
              this_build.handle_response_data(json);
          }
      });
  },

  handle_response_data : function(response) {
      var parsed_data = {
              failed_tests : response.failedTestCount,
              total_tests : response.successfulTestCount + response.failedTestCount
      };

      this.data = parsed_data;
  }
}

Next we create a test for the jenkins build:

1
2
3
4
5
6
7
8
9
10
11
12
13
it("should parse the jenkins data into a build data object", function(){

  // use a spy so we don't use a live ajax request
  spyOn($, "ajax").andCallFake(function(options) {
      options.success(jenkins_response);
  });

  var jenkins_build = dashboard.groups[0].builds[0];
  jenkins_build.refresh();

  expect(jenkins_build.data.failed_tests).toEqual(3);
  expect(jenkins_build.data.total_tests).toEqual(199);
});

This causes us to write the simplest solution, which is an if block on the build type:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
handle_response_data : function(response) {
  if(this.type == dashboard.build_types.bamboo){
      var parsed_data = {
              failed_tests : response.failedTestCount,
              total_tests : response.successfulTestCount + response.failedTestCount
      };
  } else if (this.type == dashboard.build_types.jenkins) {
      var parsed_data = {
          failed_tests : response.failCount,
          total_tests : response.totalCount
      };
  }
  this.data = parsed_data;
}

Now that our test are green we can refactor to get rid of the if block. I like to take it one step at a time, starting with bamboo. We extract the parsing logic into a method on the type object for bamboo.

1
2
3
4
5
6
7
8
9
10
11
handle_response_data : function(response) {
  if(this.type == dashboard.build_types.bamboo){
      var parsed_data = this.type.parse_raw_response(response);
  } else if (this.type == dashboard.build_types.jenkins) {
      var parsed_data = {
          failed_tests : response.failCount,
          total_tests : response.totalCount
      };
  }
  this.data = parsed_data;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
build_types : {
  jenkins : {
      get_rest_url : function(url) {
          return url + "/lastBuild/testReport/api/json?jsonp=?";
      }
  },
  bamboo : {
      get_rest_url : function(url) {
          return url.replace(/\/browse\//,"\/rest\/api\/latest\/result\/") + "-latest.json?jsonp-callback=?"
      },
      parse_raw_response : function(response){
          var parsed_data = {
                  failed_tests : response.failedTestCount,
                  total_tests : response.successfulTestCount + response.failedTestCount
          };
          return parsed_data;
      }
  }
}

After that baby step, the tests should still pass and we can move on. We can completely blow away the if block in handle_response_data and just call the parse_raw_response on the type object.

1
2
3
handle_response_data : function(response) {
  this.data = this.type.parse_raw_response(response);
}

Now we can extract the jenkins block into the jenkins build type object.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
build_types : {
  jenkins : {
      get_rest_url : function(url) {
          return url + "/lastBuild/testReport/api/json?jsonp=?";
      },
      parse_raw_response : function(response){
          return {
              failed_tests : response.failCount,
              total_tests : response.totalCount
          };
      }
  },
  bamboo : {
      get_rest_url : function(url) {
          return url.replace(/\/browse\//,"\/rest\/api\/latest\/result\/") + "-latest.json?jsonp-callback=?"
      },
      parse_raw_response : function(response){
          return {
                  failed_tests : response.failedTestCount,
                  total_tests : response.successfulTestCount + response.failedTestCount
          };
      }
  }
}