Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Custom background limits for Doughnut (Gauge)

I'm using a Gauge chart based on (Chartjs-tsgauge). I want to set background colors for chart separate from gauge limits. The problem with how Charts.JS renders background because the plugin I used doesn't have a code about backgrounds. For example, I have a Gauge with limits [0, 20, 40, 60, 80, 100]. I want to set [0-30] to green, [30-70] to yellow and [70-100] to red. Current code: CodePEN

Here is my current options.

var ctx = document.getElementById("canvas").getContext("2d");
new Chart(ctx, {
    type: "tsgauge",
    data: {
        datasets: [{
            backgroundColor: ["#0fdc63", "#fd9704", "#ff7143"],
            borderWidth: 0,
            gaugeData: {
                value: 7777,
                valueColor: "#ff7143"
            },
            gaugeLimits: [0, 3000, 7000, 10000]
        }]
    },
    options: {
            events: [],
            showMarkers: true
    }
});

And here is my approach for setting background colors.

new Chart(ctx, {
    type: "tsgauge",
    data: {
        datasets: [{
            backgroundColor: ["#0fdc63", "#fd9704", "#ff7143"],
            borderWidth: 0,
            gaugeData: {
                value: 50,
                valueColor: "#ff7143"
            },
            gaugeLimits: [0, 20, 40, 60, 80, 100],
            gaugeColors: [{
                min: 0,
                max: 30,
                color: ""
                }, {
                min: 30,
                max: 70,
                color: ""
             },{
                min:70,
                max:100,
                color: ""
             }]
         }]
    },
    options: {
        events: [],
        showMarkers: true
    }
});

Currently Chart.JS matches colors 0 to i with limits 0 to i. I also thought drawing another dummy chart with desired colors and set it on top of the real chart but it seems dodgy way of doing this.

like image 647
kenarsuleyman Avatar asked Nov 14 '19 12:11

kenarsuleyman


People also ask

How do you show values on a pie chart in Chartjs?

To display pie chart data values of each slice in Chart. js and JavaScript, we can use the chartjs-plugin-labels plugin. to add the script tags for Chart. js, the plugin, and the canvas for the chart.


Video Answer


1 Answers

Previous Solution

You can change your dataset a bit to achieve that:

backgroundColor: ["green", "green", "green", "yellow", "yellow", "yellow", "yellow", "red", "red", "red"],
gaugeLimits: [0, 10, 20, 30, 40, 50, 60, 70, 80, 90, 100],

Since your gagueLimits are split into a range of 20 currently, so you can't access the limit of 30,

So, what I did here is elaborated the guageLimits to a split range of 10 and now 30 is accessible, so is 70,

Now about the colors, each of your guageLimits require a color in the backgroundColor array, I set them accordingly, check the above array. Since none of the corresponding colors can be empty, you might need to use the same color until you reach the next color range.

Updated Solution

Your requirements cleared now:

  • You want the gauge labels to not be too cozy, which is why a limit of 20.
  • And you want to separate the color ranges to 30 and 70 points.

Now, technically if you want to set a color range to a specific point, you need to have that value on the gaugeLimits array, so your reduced gaugeLimits array now becomes like this:

gaugeLimits: [0, 20, 30, 40, 60, 70, 80, 100],

Well, no you can set the colors accordingly since you have the points specified in the array. Hence, the backgroundColor array is:

backgroundColor: ["#0fdc63", "#0fdc63", "#fd9704", "#fd9704", "#fd9704", "#ff7143", "#ff7143"]

Now, the final Trick

You still need to hide the 30, and 70 from the scale, I tried to toggle the showMarkers in your code, looks like it either enables or disables the visible number scale entirely, I tried to provide an array of strings, but it only accepts boolean


Chartjs-tsgauge Doesn't provide much documentation about available options, so I snuck into the code and found markerFormatFn

What it does is take a function as a parameter and you can provide a function telling what to do with each of your marker items.

So, I came up with an idea to provide a function to only show the numbers which are dividable by 20 without any remainder, otherwise return empty string, here it is:

markerFormatFn: n => n % 20 === 0 ? n.toString() : '',

Check the Snippet:

//Gauge Plugin
(function() {
  if (!window.Chart) {
    return;
  }

  function GaugeChartHelper() {}
  GaugeChartHelper.prototype.setup = function(chart, config) {
    this.chart = chart;
    this.ctx = chart.ctx;
    this.limits = config.data.datasets[0].gaugeLimits;
    this.data = config.data.datasets[0].gaugeData;
    var options = chart.options;
    this.fontSize = options.defaultFontSize;
    this.fontStyle = options.defaultFontFamily;
    this.fontColor = options.defaultFontColor;
    this.ctx.textBaseline = "alphabetic";
    this.arrowAngle = 25 * Math.PI / 180;
    this.arrowColor = config.options.indicatorColor || options.arrowColor;
    this.showMarkers =
      typeof config.options.showMarkers === "undefined" ?
      true :
      config.options.showMarkers;
    if (config.options.markerFormatFn) {
      this.markerFormatFn = config.options.markerFormatFn;
    } else {
      this.markerFormatFn = function(value) {
        return value;
      };
    }
  };
  GaugeChartHelper.prototype.applyGaugeConfig = function(chartConfig) {
    this.calcLimits();
    chartConfig.data.datasets[0].data = this.doughnutData;
    var ctx = this.ctx;
    var labelsWidth = this.limits.map(
      function(label) {
        var text = this.markerFormatFn(label);
        return ctx.measureText(text).width;
      }.bind(this)
    );
    var padding = Math.max.apply(this, labelsWidth) + this.chart.width / 35;
    var heightRatio = this.chart.height / 50;
    chartConfig.options.layout.padding = {
      top: this.fontSize + heightRatio,
      left: padding,
      right: padding,
      bottom: heightRatio * 2
    };
  };
  GaugeChartHelper.prototype.calcLimits = function() {
    var limits = this.limits;
    var data = [];
    var total = 0;
    for (var i = 1, ln = limits.length; i < ln; i++) {
      var dataValue = Math.abs(limits[i] - limits[i - 1]);
      total += dataValue;
      data.push(dataValue);
    }
    this.doughnutData = data;
    var minValue = limits[0];
    var maxValue = limits[limits.length - 1];
    this.isRevers = minValue > maxValue;
    this.minValue = this.isRevers ? maxValue : minValue;
    this.totalValue = total;
  };
  GaugeChartHelper.prototype.updateGaugeDimensions = function() {
    var chartArea = this.chart.chartArea;
    this.gaugeRadius = this.chart.innerRadius;
    this.gaugeCenterX = (chartArea.left + chartArea.right) / 2;
    this.gaugeCenterY =
      (chartArea.top + chartArea.bottom + this.chart.outerRadius) / 2;
    this.arrowLength = this.chart.radiusLength * 2;
  };
  GaugeChartHelper.prototype.getCoordOnCircle = function(r, alpha) {
    return {
      x: r * Math.cos(alpha),
      y: r * Math.sin(alpha)
    };
  };
  GaugeChartHelper.prototype.getAngleOfValue = function(value) {
    var result = 0;
    var gaugeValue = value - this.minValue;
    if (gaugeValue <= 0) {
      result = 0;
    } else if (gaugeValue >= this.totalValue) {
      result = Math.PI;
    } else {
      result = Math.PI * gaugeValue / this.totalValue;
    }
    if (this.isRevers) {
      return Math.PI - result;
    } else {
      return result;
    }
  };
  GaugeChartHelper.prototype.renderLimitLabel = function(value) {
    var ctx = this.ctx;
    var angle = this.getAngleOfValue(value);
    var coord = this.getCoordOnCircle(
      this.chart.outerRadius + this.chart.radiusLength / 2,
      angle
    );
    var align;
    var diff = angle - Math.PI / 2;
    if (diff > 0) {
      align = "left";
    } else if (diff < 0) {
      align = "right";
    } else {
      align = "center";
    }
    ctx.textAlign = align;
    ctx.font = this.fontSize + "px " + this.fontStyle;
    ctx.fillStyle = this.fontColor;
    var text = this.markerFormatFn(value);
    ctx.fillText(
      text,
      this.gaugeCenterX - coord.x,
      this.gaugeCenterY - coord.y
    );
  };
  GaugeChartHelper.prototype.renderLimits = function() {
    for (var i = 0, ln = this.limits.length; i < ln; i++) {
      this.renderLimitLabel(this.limits[i]);
    }
  };
  GaugeChartHelper.prototype.renderValueLabel = function() {
    var label = this.data.value.toString();
    var ctx = this.ctx;
    ctx.font = "30px " + this.fontStyle;
    var stringWidth = ctx.measureText(label).width;
    var elementWidth = 0.75 * this.gaugeRadius * 2;
    var widthRatio = elementWidth / stringWidth;
    var newFontSize = Math.floor(30 * widthRatio);
    var fontSizeToUse = Math.min(newFontSize, this.gaugeRadius);
    ctx.textAlign = "center";
    ctx.font = fontSizeToUse + "px " + this.fontStyle;
    ctx.fillStyle = this.data.valueColor || this.fontColor;
    ctx.fillText(label, this.gaugeCenterX, this.gaugeCenterY);
  };
  GaugeChartHelper.prototype.renderValueArrow = function(value) {
    var angle = this.getAngleOfValue(
      typeof value === "number" ? value : this.data.value
    );
    this.ctx.globalCompositeOperation = "source-over";
    this.renderArrow(
      this.gaugeRadius,
      angle,
      this.arrowLength,
      this.arrowAngle,
      this.arrowColor
    );
  };
  GaugeChartHelper.prototype.renderSmallValueArrow = function(value) {
    var angle = this.getAngleOfValue(value);
    this.ctx.globalCompositeOperation = "source-over";
    this.renderArrow(
      this.gaugeRadius - 1,
      angle,
      this.arrowLength - 1,
      this.arrowAngle,
      this.arrowColor
    );
  };
  GaugeChartHelper.prototype.clearValueArrow = function(value) {
    var angle = this.getAngleOfValue(value);
    this.ctx.lineWidth = 2;
    this.ctx.globalCompositeOperation = "destination-out";
    this.renderArrow(
      this.gaugeRadius - 1,
      angle,
      this.arrowLength + 1,
      this.arrowAngle,
      "#FFFFFF"
    );
    this.ctx.stroke();
  };
  GaugeChartHelper.prototype.renderArrow = function(
    radius,
    angle,
    arrowLength,
    arrowAngle,
    arrowColor
  ) {
    var coord = this.getCoordOnCircle(radius, angle);
    var arrowPoint = {
      x: this.gaugeCenterX - coord.x,
      y: this.gaugeCenterY - coord.y
    };
    var ctx = this.ctx;
    ctx.fillStyle = arrowColor;
    ctx.beginPath();
    ctx.moveTo(arrowPoint.x, arrowPoint.y);
    coord = this.getCoordOnCircle(arrowLength, angle + arrowAngle);
    ctx.lineTo(arrowPoint.x + coord.x, arrowPoint.y + coord.y);
    coord = this.getCoordOnCircle(arrowLength, angle - arrowAngle);
    ctx.lineTo(arrowPoint.x + coord.x, arrowPoint.y + coord.y);
    ctx.closePath();
    ctx.fill();
  };
  GaugeChartHelper.prototype.animateArrow = function() {
    var stepCount = 30;
    var animateTimeout = 300;
    var gaugeValue = this.data.value - this.minValue;
    var step = gaugeValue / stepCount;
    var i = 0;
    var currentValue = this.minValue;
    var interval = setInterval(
      function() {
        i++;
        this.clearValueArrow(currentValue);
        if (i > stepCount) {
          clearInterval(interval);
          this.renderValueArrow();
        } else {
          currentValue += step;
          this.renderSmallValueArrow(currentValue);
        }
      }.bind(this),
      animateTimeout / stepCount
    );
  };
  Chart.defaults.tsgauge = {
    animation: {
      animateRotate: true,
      animateScale: false
    },
    cutoutPercentage: 95,
    rotation: Math.PI,
    circumference: Math.PI,
    legend: {
      display: false
    },
    scales: {},
    arrowColor: "#444"
  };
  Chart.controllers.tsgauge = Chart.controllers.doughnut.extend({
    initialize: function(chart) {
      var gaugeHelper = (this.gaugeHelper = new GaugeChartHelper());
      gaugeHelper.setup(chart, chart.config);
      gaugeHelper.applyGaugeConfig(chart.config);
      chart.config.options.animation.onComplete = function(chartElement) {
        gaugeHelper.updateGaugeDimensions();
        gaugeHelper.animateArrow();
      };
      Chart.controllers.doughnut.prototype.initialize.apply(this, arguments);
    },
    draw: function() {
      Chart.controllers.doughnut.prototype.draw.apply(this, arguments);
      var gaugeHelper = this.gaugeHelper;
      gaugeHelper.updateGaugeDimensions();
      gaugeHelper.renderValueLabel();
      if (gaugeHelper.showMarkers) {
        gaugeHelper.renderLimits();
      }
      gaugeHelper.renderSmallValueArrow(gaugeHelper.minValue);
    }
  });
})();

//Chart setup

var ctx = document.getElementById("chart3").getContext("2d");
new Chart(ctx, {
  type: "tsgauge",
  data: {
    datasets: [{
      backgroundColor: ["#0fdc63", "#0fdc63", "#fd9704", "#fd9704", "#fd9704", "#ff7143", "#ff7143"],
      borderWidth: 0,
      gaugeData: {
        value: 50,
        valueColor: "#ff7143"
      },
      gaugeLimits: [0, 20, 30, 40, 60, 70, 80, 100],
    }]
  },
  options: {
    events: [],
    showMarkers: true,
    markerFormatFn: n => n % 20 === 0 ? n.toString() : '',
  }
});
.gauge {
  width: 500px;
  height: 400px;
}
<link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/Chart.min.css" rel="stylesheet" />
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/Chart.bundle.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.4.1/jquery.min.js"></script>


<div class="gauge">
  <canvas id="chart3"></canvas>
</div>
like image 63
Towkir Avatar answered Sep 30 '22 11:09

Towkir