甜甜圈(仪表)的自定义背景限制

Posted

技术标签:

【中文标题】甜甜圈(仪表)的自定义背景限制【英文标题】:Custom background limits for Doughnut (Gauge) 【发布时间】:2020-03-10 09:32:16 【问题描述】:

我正在使用基于 (Chartjs-tsgauge) 的仪表图表。 我想为图表设置与仪表限制分开的背景颜色。 Charts.JS 渲染背景的问题,因为我使用的插件没有关于背景的代码。 例如,我有一个限制为[0, 20, 40, 60, 80, 100] 的仪表。我想将[0-30] 设置为绿色,[30-70] 设置为黄色,[70-100] 设置为红色。 当前代码:CodePEN

这是我目前的选择。

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
    
);

这是我设置背景颜色的方法。

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
    
);

目前Chart.JS 匹配颜色0i,限制0i。 我还想绘制另一个具有所需颜色的虚拟图表并将其设置在真实图表的顶部,但这样做似乎很狡猾。

【问题讨论】:

您好,我想帮助您,但我认为我并没有完全按照您的意愿去做,您能否添加一张您想要获得的预期输出的图像?您指的是图表背后的哪个背景? 【参考方案1】:

以前的解决方案

你可以稍微改变你的数据集来实现:

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

由于你的gagueLimits目前被分成20的范围,所以你不能访问30的限制,

所以,我在这里所做的是将guageLimits 细化为10 的分割范围,现在30 可以访问,70 也可以访问,

现在关于颜色,您的每个guageLimits 都需要backgroundColor 数组中的颜色,我相应地设置它们,检查上面的数组。由于对应的颜色都不能为空,因此您可能需要使用相同的颜色,直到达到下一个颜色范围。

更新的解决方案

您的要求现已清除:

您希望仪表标签不要太舒适,这就是为什么限制为 20 个。 并且您希望将颜色范围分隔为 30 和 70 点。

现在,从技术上讲,如果您想将颜色范围设置为特定点,则需要在 gaugeLimits 数组上拥有该值,因此您的缩减后的 gaugeLimits 数组现在变成这样:

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

好吧,不,您可以相应地设置颜色,因为您在数组中指定了点。因此,backgroundColor 数组是:

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

现在,最后一招

您仍然需要从比例中隐藏 30 和 70,我尝试在代码中切换 showMarkers,看起来它完全启用或禁用可见数字比例,我尝试提供字符串数组,但它只接受boolean


Chartjs-tsgauge 没有提供太多关于可用选项的文档,所以我潜入代码并找到了markerFormatFn

它的作用是将函数作为参数,您可以提供一个函数来告诉您如何处理每个标记项。

所以,我想出了一个想法,提供一个函数,只显示可被 20 整除的数字,没有任何余数,否则返回空字符串,这里是:

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

检查代码段:

//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/chart.js@2.9.2/dist/Chart.min.css" rel="stylesheet" />
<script src="https://cdn.jsdelivr.net/npm/chart.js@2.9.2/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>

【讨论】:

“现在关于颜色,你的每个 guageLimits 都需要 backgroundColor 数组中的颜色”这就是我不想做的事情。目前,在绘制 guageLimits 时会检查其索引并使用 backgroundColor[index] 作为其背景颜色。我想根据问题中看到的 gaugeColors 属性绘制背景。如果你好奇我为什么要这个?因为我不想在图表上显示太多标签,例如“10-15-20-25-30-35...等”。 是的,它运行良好。非常感谢您的努力。我太忙于另一份工作,对此感到抱歉。

以上是关于甜甜圈(仪表)的自定义背景限制的主要内容,如果未能解决你的问题,请参考以下文章

使用nvd3.js自定义甜甜圈图

圆形仪表,指示器(使用甜甜圈作为温度计样式的替代品)

WordPress仪表板上的自定义内容

WordPress 和高级自定义字段管理,仪表板中的自定义列

为啥 Firebase 分析的自定义事件未显示在仪表板上?

馅饼甜甜圈高图的内部区域颜色