[Chartjs]-Chart js – Draw center of each bubbles of a bubble chart

1👍

Chart.js has always been a very straight forward and simple library. One of the advantages this has is that it is easy to customize. This is also one of the good points of JavaScript, source code is always available for what ever library you use.

There is how ever one draw back with customization. Once you have made changes to the library you will have to use that particular version because customization is not something that the authors will consider when making changes.

So to use the code below you should go to the github page and download the project and use that version of chart.js for your site. I dont change the original, most of the time each customization is specific for a particular case and the code is customised at the clients side.

The change is very simple. First backup the function that draws a point

Chart.canvasHelpers.defaultDrawPoint = Chart.canvasHelpers.drawPoint;

This is done so you can pass on all calls that you are not interested in back to the standard handler.

Next write the intercept code by replacing the function you just backed up.

Chart.canvasHelpers.drawPoint = function(ctx, pointStyle, radius, x, y){

Looking at the original source code you can work out how it draws the circles and just trap that behavior passing on all other argument variants to the original.

If pointStyle is undefined or === “circle” you handle that your self

    // pointStyle undefined is default 
    // pointStyle === "circle" is the default named style
    if(pointStyle === undefined || pointStyle === "circle"){

        // next 4 lines copied from the source
        ctx.beginPath();
        ctx.arc(x, y, radius, 0, Math.PI * 2);
        ctx.closePath();
        ctx.fill();   

You then add your custom code to render what ever you like. It is important that you save the current 2D context as you dont want to have to worry that you break something further down the line.

        // draw a cross

        ctx.save();  // save the state
        ctx.strokeStyle = "white";
        ctx.strokeWidth = 4;
        ctx.beginPath();
        ctx.moveTo(x - radius *0.3, y - radius *0.3);
        ctx.lineTo(x + radius *0.3, y + radius *0.3);
        ctx.moveTo(x + radius *0.3, y - radius *0.3);
        ctx.lineTo(x - radius *0.3, y + radius *0.3);
        ctx.stroke();

Then restore the state of the 2D context

        ctx.restore(); // restore the state

The else handles the standard calls you are not interested in

    }else{  // all other styles pass on to default handler
        Chart.canvasHelpers.defaultDrawPoint(ctx, pointStyle, radius, x, y);
    }

For Chart.js this is particularly useful as it gives a way to customize and get the animations as well.

Chart.canvasHelpers.defaultDrawPoint = Chart.canvasHelpers.drawPoint;
Chart.canvasHelpers.drawPoint = function(ctx, pointStyle, radius, x, y){
    // PointStyle undefined is default 
    // PointStyle === "circle" is the default named style
    if(pointStyle === undefined || pointStyle === "circle"){
    
        ctx.beginPath();
        ctx.arc(x, y, radius, 0, Math.PI * 2);
        ctx.closePath();
        ctx.fill();   
        
        // custom code here
        ctx.save();  // save the state
        ctx.strokeStyle = "white";
        ctx.strokeWidth = 4;
        ctx.beginPath();
        ctx.moveTo(x - radius *0.3, y - radius *0.3);
        ctx.lineTo(x + radius *0.3, y + radius *0.3);
        ctx.moveTo(x + radius *0.3, y - radius *0.3);
        ctx.lineTo(x - radius *0.3, y + radius *0.3);
        ctx.stroke();
        ctx.restore(); // restor the state
        
    }else{  // all other styles pass on to default handler
        Chart.canvasHelpers.defaultDrawPoint(ctx, pointStyle, radius, x, y);
    }
}

// some utils to add data
// returns a random int
const rand = (min, max = min + (min = 0))=> Math.floor( Math.random() * (max-min) + min);    
// returns a random data point {x,y,r}
const randData = ()=>({x : rand(0,50), y: rand(5,50), r: rand(4,20)});
// create a chart.
const ctx = canvas.getContext("2d");
const chart = new Chart(ctx, {
   type: "bubble",
   data: {
      datasets: [{
         label: "Random Data",
         backgroundColor: "#7AF",
         data: (()=>{
            var dat = [];
            for(var i = 0; i < 10; i++){ dat.push(randData()) }
            return dat;
         })(),
      }]
   },
   options: { responsive: false } // this is required to work or it throws
                                  // not sure why by it is not due to the
                                  // changes
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/2.5.0/Chart.min.js"></script>
<canvas id=canvas height=200 width=400></canvas>

To roll back the changes simply set the function back to the original.

Chart.canvasHelpers.drawPoint = Chart.canvasHelpers.defualtDrawPoint;

And remove the extra reference

Chart.canvasHelpers.defualtDrawPoint = undefined;

2👍

Your question seemed quite interesting to me, therefore, I constructed the following chartjs plugin, that will help fulfill your requirement.

Chart.plugins.register({
   afterDraw: c => {
      let datasets = c.data.datasets;
      datasets.forEach((e, i) => {
         let isHidden = e._meta[0].hidden;
         if (!isHidden) {
            let data = c.getDatasetMeta(i).data;
            data.forEach(e => {
               let ctx = c.chart.ctx;
               let x = e._model.x;
               let y = e._model.y;
               let r = e._model.radius;

               // draw a cross
               // or you can draw anything using general canvas methods
               ctx.save();
               ctx.beginPath();
               ctx.moveTo(x - r / 4, y - r / 4);
               ctx.lineTo(x + r / 4, y + r / 4);
               ctx.moveTo(x + r / 4, y - r / 4);
               ctx.lineTo(x - r / 4, y + r / 4);
               ctx.strokeStyle = 'white';
               ctx.lineWidth = 2;
               ctx.stroke();
               ctx.restore();
            });
         }
      });
   }
});

ᴅᴇᴍᴏ

Chart.plugins.register({
   afterDraw: c => {
      let datasets = c.data.datasets;
      datasets.forEach((e, i) => {
         let isHidden = e._meta[0].hidden;
         if (!isHidden) {
            let data = c.getDatasetMeta(i).data;
            data.forEach(e => {
               let ctx = c.chart.ctx;
               let x = e._model.x;
               let y = e._model.y;
               let r = e._model.radius;
               
               // draw a cross
               // or you can draw anything using general canvas methods
               ctx.save();
               ctx.beginPath();
               ctx.moveTo(x - r / 4, y - r / 4);
               ctx.lineTo(x + r / 4, y + r / 4);
               ctx.moveTo(x + r / 4, y - r / 4);
               ctx.lineTo(x - r / 4, y + r / 4);
               ctx.strokeStyle = 'white';
               ctx.lineWidth = 2;
               ctx.stroke();
               ctx.restore();
            });
         }
      });
   }
});

let ctx = document.querySelector('#c').getContext('2d');
let chart = new Chart(ctx, {
   type: 'bubble',
   data: {
      labels: ['Jan', 'Feb', 'Mar'],
      datasets: [{
         label: 'John',
         data: [
            { x: 5, y: 5, r: 10 },
            { x: 10, y: 10, r: 15 },
            { x: 16, y: 15, r: 18 }
         ],
         backgroundColor: '#76d1bf'
      }, {
         label: 'Smith',
         data: [
            { x: 3, y: 10, r: 10 },
            { x: 7, y: 11, r: 15 },
            { x: 12, y: 6, r: 18 }
         ],
         backgroundColor: '#827ada'
      }]
   },
   options: {
      responsive: false,
      scales: {
         xAxes: [{
            ticks: {
               min: 2,
               max: 18,
               stepSize: 4
            }
      	 }],
         yAxes: [{
            ticks: {
               min: 0,
               max: 20,
               stepSize: 4
            }
      	 }]
      }
   }
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/2.5.0/Chart.min.js"></script>
<canvas id="c" height="200"></canvas>

1👍

grunt’s answer did not work for me. I don’t know if there are different formats or if it’s written in an outdated version but I have modified his solution to the following so it would work in my project. Basically I have

  • removed the register since it wasn’t in line with what the example for background-color was like and

  • found out that the data points’ info isn’t as nested anymore so I changed the way the properties are accessed, too

      plugins: [
              {
                  id: 'background-colour',
                  beforeDraw: (chart) => {
                      const ctx = chart.ctx;
                      ctx.save();
                      ctx.fillStyle = 'white';
                      ctx.fillRect(0, 0, width, height);
                      ctx.restore();
                  }
              },
              {
                  id: 'abc',
                  afterDraw: (c) => {
                      let datasets = c.data.datasets;
    
                      datasets.forEach((e, i) => {
    
                          let isHidden = e.hidden;
                          if (!isHidden) {
                              let data = c.getDatasetMeta(i).data;
                              data.forEach(e => {
                                  let ctx = c.ctx;
                                  let x = e.x;
                                  let y = e.y;
                                  let r = e.options.radius as number;
    
                                  ctx.save();
                                  ctx.beginPath();
                                  ctx.moveTo(x - r / 4, y - r / 4);
                                  ctx.lineTo(x + r / 4, y + r / 4);
                                  ctx.moveTo(x + r / 4, y - r / 4);
                                  ctx.lineTo(x - r / 4, y + r / 4);
                                  ctx.strokeStyle = 'white';
                                  ctx.lineWidth = 2;
                                  ctx.stroke();
                                  ctx.restore();
                              });
                          }
                      });
                  }
              }
          ]
    

Leave a comment