20

I created a donut chart with Chart.js and I want it to have rounded edges at both ends. I want it to be like this:

The way I want it

But I have it like this, with sharp edges:

I have this

The best I found was this answer: How to put rounded corners on a Chart.js Bar chart, but it is for bar charts, and I have no clue of how to adapt it for doughnuts..

Here is my code:

HTML

<div class="modal-div-canvas js-chart">
  <div class="chart-canvas">
     <canvas id="openedCanvas" width="1" height="1"></canvas>
        <div class="chart-background"></div>
            <span class="chart-unique-value">
                 <span class="js-count">
                    85
                 </span>
                 <span class="cuv-percent">%</span>
            </span>
        </div>
  </div>

JS

var deliveredData = {
        labels: [
            "Value"
        ],
        datasets: [
            {
                data: [85, 15)],
                backgroundColor: [
                    "#3ec556",
                    "rgba(0,0,0,0)"
                ],
                hoverBackgroundColor: [
                    "#3ec556",
                    "rgba(0,0,0,0)"
                ],
                borderWidth: [
                    0, 0
                ]
            }]
    };

    var deliveredOpt = {
        cutoutPercentage: 88,
        animation: {
            animationRotate: true,
            duration: 2000
        },
        legend: {
            display: false
        },
        tooltips: {
            enabled: false
        }
    };

   var chart = new Chart($('#openedCanvas'), {
        type: 'doughnut',
        data: deliveredData,
        options: deliveredOpt
    });
}};

Someone know how to do this?

Community
  • 1
  • 1
jpenna
  • 8,426
  • 5
  • 28
  • 36

6 Answers6

20

You can extend the chart to do this


Preview

enter image description here


Script

Chart.defaults.RoundedDoughnut = Chart.helpers.clone(Chart.defaults.doughnut);
Chart.controllers.RoundedDoughnut = Chart.controllers.doughnut.extend({
    draw: function (ease) {
        var ctx = this.chart.chart.ctx;

        var easingDecimal = ease || 1;
        Chart.helpers.each(this.getDataset().metaData, function (arc, index) {
            arc.transition(easingDecimal).draw();

            var vm = arc._view;
            var radius = (vm.outerRadius + vm.innerRadius) / 2;
            var thickness = (vm.outerRadius - vm.innerRadius) / 2;
            var angle = Math.PI - vm.endAngle - Math.PI / 2;

            ctx.save();
            ctx.fillStyle = vm.backgroundColor;
            ctx.translate(vm.x, vm.y);
            ctx.beginPath();
            ctx.arc(radius * Math.sin(angle), radius * Math.cos(angle), thickness, 0, 2 * Math.PI);
            ctx.arc(radius * Math.sin(Math.PI), radius * Math.cos(Math.PI), thickness, 0, 2 * Math.PI);
            ctx.closePath();
            ctx.fill();
            ctx.restore();
        });
    },
});

and then

...
type: 'RoundedDoughnut',
...

Stack Snippet

Chart.defaults.RoundedDoughnut = Chart.helpers.clone(Chart.defaults.doughnut);
        Chart.controllers.RoundedDoughnut = Chart.controllers.doughnut.extend({
            draw: function (ease) {
              var ctx = this.chart.chart.ctx;
                
                var easingDecimal = ease || 1;
                Chart.helpers.each(this.getDataset().metaData, function (arc, index) {
                    arc.transition(easingDecimal).draw();

                    var vm = arc._view;
                    var radius = (vm.outerRadius + vm.innerRadius) / 2;
                    var thickness = (vm.outerRadius - vm.innerRadius) / 2;
                    var angle = Math.PI - vm.endAngle - Math.PI / 2;
                    
                    ctx.save();
                    ctx.fillStyle = vm.backgroundColor;
                    ctx.translate(vm.x, vm.y);
                    ctx.beginPath();
                    ctx.arc(radius * Math.sin(angle), radius * Math.cos(angle), thickness, 0, 2 * Math.PI);
                    ctx.arc(radius * Math.sin(Math.PI), radius * Math.cos(Math.PI), thickness, 0, 2 * Math.PI);
                    ctx.closePath();
                    ctx.fill();
                    ctx.restore();
                });
            },
        });

        var deliveredData = {
            labels: [
                "Value"
            ],
            datasets: [
                {
                    data: [85, 15],
                    backgroundColor: [
                        "#3ec556",
                        "rgba(0,0,0,0)"
                    ],
                    hoverBackgroundColor: [
                        "#3ec556",
                        "rgba(0,0,0,0)"
                    ],
                    borderWidth: [
                        0, 0
                    ]
                }]
        };

        var deliveredOpt = {
            cutoutPercentage: 88,
            animation: {
                animationRotate: true,
                duration: 2000
            },
            legend: {
                display: false
            },
            tooltips: {
                enabled: false
            }
        };

        var chart = new Chart($('#openedCanvas'), {
            type: 'RoundedDoughnut',
            data: deliveredData,
            options: deliveredOpt
        });
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/2.0.1/Chart.bundle.min.js"></script>

<canvas id="openedCanvas" height="230" width="680"></canvas>
potatopeelings
  • 40,709
  • 7
  • 95
  • 119
  • 2
    In fact, with value = 0, there is a green ball on the top of the graph, but it should show nothing. I solved it with a statement saying that if value == 0, then use 'doughnut', else 'RoundedDoughnut'. Again, thanks a lot! – jpenna May 04 '16 at 05:00
  • Ah, I hadn't considered 0. Cheers! – potatopeelings May 04 '16 at 06:56
  • This is great. I would need such effect for more datasets. Let's take basic example of [300, 50, 100] they use on documentation page. I get something like this http://take.ms/JRqgh, any idea @potatopeelings how to achieve that? My goal is something like this http://take.ms/thRIb – Dmonix Jul 25 '16 at 13:20
  • In typescript i get this error TS2339: Property 'helpers' does not exist on type 'typeof Chart'. – Dimitrios Filippou May 21 '18 at 11:15
  • 2
    Does this solution still work for the current version of chart.js? – Megaroeny May 29 '18 at 17:54
  • This doesn't work in this new version. Does anyone knows how to achieve the same result? – Gabriel Bueno Dec 05 '18 at 12:04
  • 10
    @GabrielBueno @jaminroe To get it working in the current version of chart.js: replace `this.getDataset().metaData` with `this.getMeta().data` – Jordy van den Aardweg Jan 02 '19 at 10:03
  • any way we can make this work with borderWidth also ? – Vikhyath Maiya Nov 02 '20 at 04:43
  • As of chartjs 3.x you can simply do: `new Chart({ type: 'doughnut', options: { borderRadius: 10 } ... })` – Mr.Toxy Apr 20 '21 at 17:59
15

I made some changes in the @potatopeeling snippet, I made compatibility with the newer (2.9.x) version of chart.js also fixed where the "startArc" should be rendered and the color from the previous segment to match this "startArc", so we can have more than 2 segments. This is the result:

enter image description here

Chart.defaults.RoundedDoughnut    = Chart.helpers.clone(Chart.defaults.doughnut);
Chart.controllers.RoundedDoughnut = Chart.controllers.doughnut.extend({
    draw: function(ease) {
        var ctx           = this.chart.ctx;
        var easingDecimal = ease || 1;
        var arcs          = this.getMeta().data;
        Chart.helpers.each(arcs, function(arc, i) {
            arc.transition(easingDecimal).draw();

            var pArc   = arcs[i === 0 ? arcs.length - 1 : i - 1];
            var pColor = pArc._view.backgroundColor;

            var vm         = arc._view;
            var radius     = (vm.outerRadius + vm.innerRadius) / 2;
            var thickness  = (vm.outerRadius - vm.innerRadius) / 2;
            var startAngle = Math.PI - vm.startAngle - Math.PI / 2;
            var angle      = Math.PI - vm.endAngle - Math.PI / 2;

            ctx.save();
            ctx.translate(vm.x, vm.y);

            ctx.fillStyle = i === 0 ? vm.backgroundColor : pColor;
            ctx.beginPath();
            ctx.arc(radius * Math.sin(startAngle), radius * Math.cos(startAngle), thickness, 0, 2 * Math.PI);
            ctx.fill();

            ctx.fillStyle = vm.backgroundColor;
            ctx.beginPath();
            ctx.arc(radius * Math.sin(angle), radius * Math.cos(angle), thickness, 0, 2 * Math.PI);
            ctx.fill();

            ctx.restore();
        });
    }
});

window.onload = function() {
    new Chart(document.getElementById('usersChart'), {
        type   : 'RoundedDoughnut',
        data   : {
            datasets: [
                {
                    data           : [40, 20, 20, 20],
                    backgroundColor: [
                        '#e77099',
                        '#5da4e7',
                        '#8f75e7',
                        '#8fe768'
                    ],
                    borderWidth    : 0
                }]
        },
        options: {
            cutoutPercentage: 70
        }
    });
};
<script src="https://github.com/chartjs/Chart.js/releases/download/v2.9.3/Chart.min.js"></script>
<link rel="stylesheet" href="https://github.com/chartjs/Chart.js/releases/download/v2.9.3/Chart.min.css">
<div style="width: 200px; height: 200px;">
<canvas id="usersChart" width="1" height="1"></canvas>
</div>
Dreanmer
  • 733
  • 2
  • 7
  • 18
  • Really nice additions there, one thing you haven't fixed (which the old version suffered from too) is the arc/ball still appearing on charts where a data value might be 0. Apart from that, great work! – A Friend May 06 '20 at 06:06
  • i would like to plot based on the values of the datasets.. like green pink and blue has 2 each. would like only that to come. and not the purple. with the abv code i get blue as one dot . – Preethi Rajaraman Aug 20 '20 at 08:38
  • how would you add spacing between each one? – Max Medina Oct 29 '20 at 09:58
  • any way we can make this work with borderWidth also ? – Vikhyath Maiya Nov 02 '20 at 04:43
  • any idea how to make this work with the latest v3 versions of chartjs ? – Towkir Jun 08 '21 at 16:29
  • 2
    @AFriend simply replace ```Chart.helpers.each(arcs, function(arc, i) {``` with ```Chart.helpers.each(arcs.filter(item => item.$context.parsed > 0), function(arc, i) {``` to not render colors for 0 data – Towkir Jun 09 '21 at 21:16
  • @Towkir Haven't had to test it but this looks great. Appreciate the help! – A Friend Jun 10 '21 at 22:02
3

[Adapted for Vue] If you are using Vue, use the followings: enter image description here

<script>
import { generateChart, mixins } from 'vue-chartjs';
import Chart from 'chart.js';
import { doughnutChartOptions } from './config';
import { centerTextPlugin } from '@/utils/doughnut-chart';

const { reactiveProp } = mixins;

Chart.defaults.RoundedDoughnut = Chart.helpers.clone(Chart.defaults.doughnut);
Chart.controllers.RoundedDoughnut = Chart.controllers.doughnut.extend({
  draw(ease) {
    const { ctx } = this.chart;
    const easingDecimal = ease || 1;
    const arcs = this.getMeta().data;
    Chart.helpers.each(arcs, (arc, i) => {
      arc.transition(easingDecimal).draw();
      const pArc = arcs[i === 0 ? arcs.length - 1 : i - 1];
      const pColor = pArc._view.backgroundColor;
      const vm = arc._view;
      const radius = (vm.outerRadius + vm.innerRadius) / 2;
      const thickness = (vm.outerRadius - vm.innerRadius) / 2;
      const startAngle = Math.PI - vm.startAngle - Math.PI / 2;
      const angle = Math.PI - vm.endAngle - Math.PI / 2;
      ctx.save();
      ctx.translate(vm.x, vm.y);
      ctx.fillStyle = i === 0 ? vm.backgroundColor : pColor;
      ctx.beginPath();
      ctx.arc(radius * Math.sin(startAngle), radius * Math.cos(startAngle), thickness, 0, 2 * Math.PI);
      ctx.fill();
      ctx.fillStyle = vm.backgroundColor;
      ctx.beginPath();
      ctx.arc(radius * Math.sin(angle), radius * Math.cos(angle), thickness, 0, 2 * Math.PI);
      ctx.fill();
      ctx.restore();
    });
  },
});
const RoundedDoughnut = generateChart('custom-rounded-doughnut', 'RoundedDoughnut');

export default {
  extends: RoundedDoughnut,
  mixins: [reactiveProp],
  props: {
    data: {
      type: Object,
    },
  },
  data() {
    return {
      options: doughnutChartOptions,
    };
  },
  mounted() {
    this.addPlugin(centerTextPlugin);
    this.renderChart(this.data, this.options);
  },
};
</script>
wriozumi
  • 569
  • 6
  • 6
3

V3 answer based on answer from wahab memon but edited so it applies to all elements:

Chart.defaults.elements.arc.borderWidth = 0;
Chart.defaults.datasets.doughnut.cutout = '85%';

var chartInstance = new Chart(document.getElementById("chartJSContainer"), {
  type: 'doughnut',
  data: {
    labels: [
      'Label 1',
      'Label 2',
      'Label 3',
      'Label 4'
    ],
    datasets: [{
      label: 'My First Dataset',
      data: [22, 31, 26, 19],
      backgroundColor: [
        '#000000',
        '#ffff00',
        '#aaaaaa',
        '#ff0000'
      ]
    }]
  },

  plugins: [{
    afterUpdate: function(chart) {
      const arcs = chart.getDatasetMeta(0).data;

      arcs.forEach(function(arc) {
        arc.round = {
          x: (chart.chartArea.left + chart.chartArea.right) / 2,
          y: (chart.chartArea.top + chart.chartArea.bottom) / 2,
          radius: (arc.outerRadius + arc.innerRadius) / 2,
          thickness: (arc.outerRadius - arc.innerRadius) / 2,
          backgroundColor: arc.options.backgroundColor
        }
      });
    },
    afterDraw: (chart) => {
      const {
        ctx,
        canvas
      } = chart;

      chart.getDatasetMeta(0).data.forEach(arc => {
        const startAngle = Math.PI / 2 - arc.startAngle;
        const endAngle = Math.PI / 2 - arc.endAngle;

        ctx.save();
        ctx.translate(arc.round.x, arc.round.y);
        ctx.fillStyle = arc.options.backgroundColor;
        ctx.beginPath();
        ctx.arc(arc.round.radius * Math.sin(endAngle), arc.round.radius * Math.cos(endAngle), arc.round.thickness, 0, 2 * Math.PI);
        ctx.closePath();
        ctx.fill();
        ctx.restore();
      });
    }
  }]
});
<script src="https://cdn.jsdelivr.net/npm/chart.js@3.8.0/dist/chart.js"></script>

<body>
  <canvas id="chartJSContainer" width="200" height="200"></canvas>
</body>
LeeLenalee
  • 27,463
  • 6
  • 45
  • 69
  • If you're using Angular with ng2-charts, you can put the `plugins` array into a local variable `plugins: Plugin[];` and add it as an input into the canvas: `[plugins]="plugins"`. You need an `id: String` which is just a unique identifier for that plugin – Mingle Li Mar 22 '23 at 17:21
1

There are issues with rounding of edges of Doughnut in chart js. This package uses a Plugin to resolve the issue.

rounded-edge-donut https://www.npmjs.com/package/rounded-edge-donut

Usage https://codesandbox.io/s/uetg19?file=/src/App.js

Saad
  • 13
  • 5
  • 1
    Whilst this may theoretically answer the question, [it would be preferable](//meta.stackexchange.com/q/8259) to include the essential parts of the answer here, and provide the link for reference. Please [edit] the answer with all relevant information. – Adriaan Jul 26 '22 at 09:07
1

Since version 3 this is easily achievable via the borderRadius property on the dataset:

For example, this:

const data = {
  datasets: [
    {
      label: 'Response Rate',
      data: [completed, notCompleted],
      borderRadius: 10,
      backgroundColor: ['#48BB78', '#EDF2F7'],
      borderWidth: 0,
    },
  ],
};

Would produce this:

enter image description here

Matt B
  • 176
  • 1
  • 10