Chartjs-Chart.js – Creating a single bar chart with states

0👍

Well, I didn’t figure out a nice solution, but I found one.

So basically, as I described on the question I wanted to create a bar chart, but I also wanted that the chart would have different colors along the time…

Here’s how I solved this.

First I create and array of colors, and calculate the percentage of time a color would be visible. And then I put it inside an array of colors, called “activityGradientArray”

var generateFilteredActivityGradientArray = function (){
            var filteredActivityColorWithSpaces = $scope.filteredActivityColorWithSpaces;

            //define ratio length
            var ratioActivity = (1.0) / filteredActivityColorWithSpaces.length;

            var newRatio = 0.0;
            var oldRatio = 0.0;
            var ratioCounter = 0.0;
            var oldColor = "";
            var newColor = "";

            var colorScheme = [];

            for (var i = 0; i < filteredActivityColorWithSpaces.length; i++) {

                newColor = filteredActivityColorWithSpaces[i].color;

                if (oldColor != newColor) {
                    colorScheme.push({ratio: ratioCounter, color: newColor});

                    oldColor = newColor;
                    oldRatio = newRatio;
                }

                ratioCounter = ratioCounter + ratioActivity;
            }

            $scope.activityGradientArray = [];

            for (var i = 1; i < colorScheme.length; i++) {
                $scope.activityGradientArray.push({ratio: colorScheme[i-1].ratio, color: colorScheme[i-1].color});
                $scope.activityGradientArray.push({ratio: colorScheme[i].ratio, color: colorScheme[i-1].color});
            }

            var lastPosition = colorScheme.length-1;
            $scope.activityGradientArray.push({ratio: colorScheme[lastPosition].ratio, color: colorScheme[lastPosition].color});
            $scope.activityGradientArray.push({ratio: 1, color: colorScheme[lastPosition].color});

        };

Then I extend the Chart.controllers.line, and use the activityGradientArray to apply a gradient to the chart.chart.ctx, using addColorStop

var chartControllersLine = function(){

            return Chart.controllers.line.extend({
                update: function() {

                    if( ($scope.filteredChart.temperature.isNull()) && ($scope.filteredChart.activity.isNull()) ) {
                        return;
                    }

                    // get the min and max values
                    var minY = Math.min.apply(null, this.chart.data.datasets[$scope.CHART_DATASET_TEMPERATURE].data);
                    var maxY = Math.max.apply(null, this.chart.data.datasets[$scope.CHART_DATASET_TEMPERATURE].data);

                    $scope.minYValue = minY;

                    var yScale = this.getScaleForId(this.getDataset().yAxisID);
                    var xAxis = this.chart.scales['x-axis-0'];


                    if( (undefined == yScale) || (undefined == xAxis) ){
                        return;
                    }

                    var ctx = this.chart.chart.ctx;
                    // figure out the pixels for these and the value 0
                    var top = yScale.getPixelForValue(maxY)+5; //add
                    var bottom = yScale.getPixelForValue(minY)+5;
                    var left = xAxis.chart.chartArea.left;
                    var right = xAxis.chart.chartArea.right;

                    var minimumPixelPosition;
                    var maximumPixelPosition;
                    var minimumMarginPixelPosition;
                    var maximumMarginPixelPosition;

                    if( ($scope.homeConfig.rangeChartMin > minY) ){
                        minimumPixelPosition = yScale.getPixelForValue($scope.homeConfig.rangeChartMin);

                    } else {
                        minimumPixelPosition = yScale.getPixelForValue(minY);
                    }

                    if( ($scope.homeConfig.rangeChartMax < maxY) ) {
                        maximumPixelPosition = yScale.getPixelForValue($scope.homeConfig.rangeChartMax);
                    } else {
                        maximumPixelPosition = yScale.getPixelForValue(maxY);
                    }

                    var isAllOutside = false;
                    if( ( ($scope.homeConfig.rangeChartMax > maxY) && ($scope.homeConfig.rangeChartMin > maxY) ) ||
                        ( ($scope.homeConfig.rangeChartMax < minY) && ($scope.homeConfig.rangeChartMin < minY) ) ) {
                        isAllOutside = true;
                    }

                    var ratioMinimumTemperature = Math.abs( Math.min((minimumPixelPosition - top) / (bottom - top), 1) );
                    var ratioMaximumTemperature = Math.abs( Math.min((maximumPixelPosition - top) / (bottom - top), 1) );

                    var ratioMinimumMarginTemperature = Math.abs( Math.min(((minimumPixelPosition+5) - top) / (bottom - top), 1) );
                    var ratioMaximumMarginTemperature = Math.abs( Math.min(((maximumPixelPosition+5) - top) / (bottom - top), 1) );


                    if(ratioMinimumTemperature < 0)
                        ratioMinimumTemperature = 0;
                    if(ratioMinimumTemperature > 1)
                        ratioMinimumTemperature = 1;

                    if(ratioMaximumTemperature < 0)
                        ratioMaximumTemperature = 0;
                    if(ratioMaximumTemperature > 1)
                        ratioMaximumTemperature = 1;

                    if(!$scope.filteredChart.activity.isNull()) {

                        var gradientActivity = ctx.createLinearGradient(left, 0, right, 0);

                        for(var i = 0; i<$scope.activityGradientArray.length; i++){
                            gradientActivity.addColorStop($scope.activityGradientArray[i].ratio, $scope.activityGradientArray[i].color);
                        }
                        this.chart.data.datasets[$scope.CHART_DATASET_ACTIVITY].borderColor = gradientActivity;

                    }

                    var auxApply = Chart.controllers.line.prototype.update.apply(this, arguments);

                    return auxApply;
                }
            });

        };

Here’s is where I define the dataset:

 var lineChartData = function () {
            return  {
                xAxisID: "x-axis-0",
                labels: angular.isDefined($scope.filteredChart.labels) ? $scope.filteredChart.labels : [],
                datasets: [
                    {
                        yAxisID: "y-axis-activity",
                        label: "Activity",
                        fill: false,
                        data: angular.isDefined($scope.filteredChart.activity) ? $scope.filteredChart.activity : [],
                        borderWidth: 20,
                        pointRadius: 0,
                        borderColor: $scope.lineActivityColors,
                        borderCapStyle: 'butt',
                        borderDash: [],
                        borderDashOffset: 0.0,
                        borderJoinStyle: 'miter',
                        pointBorderWidth: 0,
                        pointHoverRadius: 5,
                        pointHitRadius: 10,

                    }]
        }


        };

And finally where I initialize the the chart with all of the above methods:

$(document).ready(function() {
                var ctx = resetHomeChart();

                if(null == ctx)
                    return;

                $scope.pointTemperatureColors = [];
                $scope.lineActivityColors = [];

                Chart.defaults.NegativeTransparentLine = Chart.helpers.clone(Chart.defaults.line);
                Chart.controllers.NegativeTransparentLine = chartControllersLine();

                $scope.lineChart = new Chart(ctx, {
                    data: lineChartData(),
                    type: 'NegativeTransparentLine',
                    pointDotRadius: 10,
                    bezierCurve: false,
                    scaleShowVerticalLines: false,
                    scaleGridLineColor: "black",
                    options: options()
                });

                var maxTemperature = Math.max.apply(Math, $scope.lineChart.data.datasets[$scope.CHART_DATASET_TEMPERATURE].data.map(function(o) {
                    return o == null ? -Infinity : o;
                }));
                var minTemperature = Math.min.apply(Math, $scope.lineChart.data.datasets[$scope.CHART_DATASET_TEMPERATURE].data.map(function(o) {
                    return o == null ? Infinity : o;
                }));

                if( (Infinity == maxTemperature || -Infinity == maxTemperature) && (Infinity == minTemperature || -Infinity == minTemperature) ){
                    maxTemperature = 0;
                    minTemperature = 0;
                }

                var yScaleMaxValue = maxTemperature + 2;
                var yScaleMinValue = minTemperature - 4;
                $scope.lineChart.options.scales.yAxes[0].ticks.max = Math.floor(yScaleMaxValue);
                $scope.lineChart.options.scales.yAxes[1].ticks.max = Math.floor(yScaleMaxValue);

                $scope.lineChart.options.scales.yAxes[0].ticks.min = Math.round(yScaleMinValue);
                $scope.lineChart.options.scales.yAxes[1].ticks.min = Math.round(yScaleMinValue);


                //adjust bar position on the chart
                for (var i = 0; i < $scope.lineChart.data.datasets[$scope.CHART_DATASET_ACTIVITY].data.length; i++) {
                    if(null != $scope.lineChart.data.datasets[$scope.CHART_DATASET_ACTIVITY].data[i]) {
                        $scope.lineChart.data.datasets[$scope.CHART_DATASET_ACTIVITY].data[i] = yScaleMinValue + 2;
                    }
                }

                $scope.lineChart.update();

            });

        };

Again, this is not an ideal solution. It takes a bit more of processing than I would expect, but it works, at least it did for me…

If anyone would have a better solution, please share it

Leave a comment