React-chartjs-2: How to extend an existing chart?

Created on 9 Jan 2019  路  8Comments  路  Source: reactchartjs/react-chartjs-2

Im looking to currently change the draw() function of the doughnut to add rounded ends. I have managed to kinda get it working with a registered plugin but not for all my datasets. Is there a way to create my own version of the doughnut?

I know in pure JS i could use something like

Chart.elements.Arc.prototype.draw = function() {
  var ctx = this._chart.ctx;
  var vm = this._view;
  var sA = vm.startAngle;
  var eA = vm.endAngle;

  ctx.beginPath();
  ctx.arc(vm.x, vm.y, vm.outerRadius, sA + window.arcSpacing, eA - window.arcSpacing);
  ctx.strokeStyle = vm.backgroundColor;
  ctx.lineWidth = vm.borderWidth;
  ctx.lineCap = 'round';
  ctx.setLineDash([15]);
  ctx.stroke();
  ctx.closePath();
};

But im not clear on how to actually extend and create my own chart to use that draw in react-chart-js.

invalid question

Most helpful comment

I'm working on a bar chart with customized rounded corners, not doughnut but I was able to extend the draw function with react-chartjs-2. The below code may give you some ideas on how to get it done.

In order to extend the draw function, usually you need to use componentWillMount(), but it's going to be deprecated in the future, so constructor() is one place to draw before render.

To have rounded bars in bar chart, you would need do something like this:

import React from 'react';
import { Bar, Chart } from 'react-chartjs-2';


class BarChart extends React.Component {
  constructor(props) {
    super(props);
  }

Chart.helpers.extend(Chart.elements.Rectangle.prototype, {
      draw() {
        const { ctx } = this._chart;
        const vm = this._view;
        let { borderWidth } = vm;

        let left; let right; let top; let bottom; let signX; let signY; let borderSkipped; let
          radius;

        // If radius is less than 0 or is large enough to cause drawing errors a max
        //      radius is imposed. If cornerRadius is not defined set it to 0.
        let { cornerRadius } = this._chart.config.options;
        if (cornerRadius < 0) { cornerRadius = 0; }

        if (typeof cornerRadius === 'undefined') { cornerRadius = 0; }

        if (!vm.horizontal) {
          // bar
          left = vm.x - vm.width / 2;
          right = vm.x + vm.width / 2;
          top = vm.y;
          bottom = vm.base;
          signX = 1;
          signY = bottom > top ? 1 : -1;
          borderSkipped = vm.borderSkipped || 'bottom';
        } else {
          // horizontal bar
          left = vm.base;
          right = vm.x;
          top = vm.y - vm.height / 2;
          bottom = vm.y + vm.height / 2;
          signX = right > left ? 1 : -1;
          signY = 1;
          borderSkipped = vm.borderSkipped || 'left';
        }

        // Canvas doesn't allow us to stroke inside the width so we can
        // adjust the sizes to fit if we're setting a stroke on the line
        if (borderWidth) {
          // borderWidth shold be less than bar width and bar height.
          const barSize = Math.min(Math.abs(left - right), Math.abs(top - bottom));
          borderWidth = borderWidth > barSize ? barSize : borderWidth;
          const halfStroke = borderWidth / 2;
          // Adjust borderWidth when bar top position is near vm.base(zero).
          const borderLeft = left + (borderSkipped !== 'left' ? halfStroke * signX : 0);
          const borderRight = right + (borderSkipped !== 'right' ? -halfStroke * signX : 0);
          const borderTop = top + (borderSkipped !== 'top' ? halfStroke * signY : 0);
          const borderBottom = bottom + (borderSkipped !== 'bottom' ? -halfStroke * signY : 0);
          // not become a vertical line?
          if (borderLeft !== borderRight) {
            top = borderTop;
            bottom = borderBottom;
          }
          // not become a horizontal line?
          if (borderTop !== borderBottom) {
            left = borderLeft;
            right = borderRight;
          }
        }

        ctx.beginPath();
        ctx.fillStyle = vm.backgroundColor;
        ctx.strokeStyle = vm.borderColor;
        ctx.lineWidth = borderWidth;

        // Corner points, from bottom-left to bottom-right clockwise
        // | 1 2 |
        // | 0 3 |
        const corners = [
          [left, bottom],
          [left, top],
          [right, top],
          [right, bottom],
        ];

        // Find first (starting) corner with fallback to 'bottom'
        const borders = ['bottom', 'left', 'top', 'right'];
        let startCorner = borders.indexOf(borderSkipped, 0);
        if (startCorner === -1) {
          startCorner = 0;
        }

        function cornerAt(index) {
          return corners[(startCorner + index) % 4];
        }

        // Draw rectangle from 'startCorner'
        let corner = cornerAt(0);
        ctx.moveTo(corner[0], corner[1]);

        for (let i = 1; i < 4; i += 1) {
          corner = cornerAt(i);
          let nextCornerId = i + 1;
          if (nextCornerId === 4) {
            nextCornerId = 0;
          }

          const width = corners[2][0] - corners[1][0];
          const height = corners[0][1] - corners[1][1];
          const x = corners[1][0];
          const y = corners[1][1];

          radius = cornerRadius;
          // Fix radius being too large
          if (radius > Math.abs(height) / 2) {
            radius = Math.floor(Math.abs(height) / 2);
          }
          if (radius > Math.abs(width) / 2) {
            radius = Math.floor(Math.abs(width) / 2);
          }

          if (height < 0) {
            // Negative values in a standard bar chart
            const xTl = x; const xTr = x + width;
            const yTl = y + height; const yTr = y + height;

            const xBl = x; const xBr = x + width;
            const yBl = y; const yBr = y;

            // Draw
            ctx.moveTo(xBl + radius, yBl);
            ctx.lineTo(xBr - radius, yBr);
            ctx.quadraticCurveTo(xBr, yBr, xBr, yBr - radius);
            ctx.lineTo(xTr, yTr + radius);
            ctx.quadraticCurveTo(xTr, yTr, xTr - radius, yTr);
            ctx.lineTo(xTl + radius, yTl);
            ctx.quadraticCurveTo(xTl, yTl, xTl, yTl + radius);
            ctx.lineTo(xBl, yBl - radius);
            ctx.quadraticCurveTo(xBl, yBl, xBl + radius, yBl);
          } else if (width < 0) {
            // Negative values in a horizontal bar chart
            const xTl = x + width; const xTr = x;
            const yTl = y; const yTr = y;

            const xBl = x + width; const xBr = x;
            const yBl = y + height; const yBr = y + height;

            // Draw
            ctx.moveTo(xBl + radius, yBl);
            ctx.lineTo(xBr - radius, yBr);
            ctx.quadraticCurveTo(xBr, yBr, xBr, yBr - radius);
            ctx.lineTo(xTr, yTr + radius);
            ctx.quadraticCurveTo(xTr, yTr, xTr - radius, yTr);
            ctx.lineTo(xTl + radius, yTl);
            ctx.quadraticCurveTo(xTl, yTl, xTl, yTl + radius);
            ctx.lineTo(xBl, yBl - radius);
            ctx.quadraticCurveTo(xBl, yBl, xBl + radius, yBl);
          } else {
            // Positive Value
            ctx.moveTo(x + radius, y);
            ctx.lineTo(x + width - radius, y);
            ctx.quadraticCurveTo(x + width, y, x + width, y + radius);
            ctx.lineTo(x + width, y + height - radius);
            ctx.quadraticCurveTo(x + width, y + height, x + width - radius, y + height);
            ctx.lineTo(x + radius, y + height);
            ctx.quadraticCurveTo(x, y + height, x, y + height - radius);
            ctx.lineTo(x, y + radius);
            ctx.quadraticCurveTo(x, y, x + radius, y);
          }
        }

        ctx.fill();
        if (borderWidth) {
          ctx.stroke();
        }
      },
    });
}

And then add a cornerRadius value to the options object, like this:

render() {
    return (
          <Bar
            data={data}
            options={{ cornerRadius: 10 }}
          />
    );
}

All 8 comments

I'm working on a bar chart with customized rounded corners, not doughnut but I was able to extend the draw function with react-chartjs-2. The below code may give you some ideas on how to get it done.

In order to extend the draw function, usually you need to use componentWillMount(), but it's going to be deprecated in the future, so constructor() is one place to draw before render.

To have rounded bars in bar chart, you would need do something like this:

import React from 'react';
import { Bar, Chart } from 'react-chartjs-2';


class BarChart extends React.Component {
  constructor(props) {
    super(props);
  }

Chart.helpers.extend(Chart.elements.Rectangle.prototype, {
      draw() {
        const { ctx } = this._chart;
        const vm = this._view;
        let { borderWidth } = vm;

        let left; let right; let top; let bottom; let signX; let signY; let borderSkipped; let
          radius;

        // If radius is less than 0 or is large enough to cause drawing errors a max
        //      radius is imposed. If cornerRadius is not defined set it to 0.
        let { cornerRadius } = this._chart.config.options;
        if (cornerRadius < 0) { cornerRadius = 0; }

        if (typeof cornerRadius === 'undefined') { cornerRadius = 0; }

        if (!vm.horizontal) {
          // bar
          left = vm.x - vm.width / 2;
          right = vm.x + vm.width / 2;
          top = vm.y;
          bottom = vm.base;
          signX = 1;
          signY = bottom > top ? 1 : -1;
          borderSkipped = vm.borderSkipped || 'bottom';
        } else {
          // horizontal bar
          left = vm.base;
          right = vm.x;
          top = vm.y - vm.height / 2;
          bottom = vm.y + vm.height / 2;
          signX = right > left ? 1 : -1;
          signY = 1;
          borderSkipped = vm.borderSkipped || 'left';
        }

        // Canvas doesn't allow us to stroke inside the width so we can
        // adjust the sizes to fit if we're setting a stroke on the line
        if (borderWidth) {
          // borderWidth shold be less than bar width and bar height.
          const barSize = Math.min(Math.abs(left - right), Math.abs(top - bottom));
          borderWidth = borderWidth > barSize ? barSize : borderWidth;
          const halfStroke = borderWidth / 2;
          // Adjust borderWidth when bar top position is near vm.base(zero).
          const borderLeft = left + (borderSkipped !== 'left' ? halfStroke * signX : 0);
          const borderRight = right + (borderSkipped !== 'right' ? -halfStroke * signX : 0);
          const borderTop = top + (borderSkipped !== 'top' ? halfStroke * signY : 0);
          const borderBottom = bottom + (borderSkipped !== 'bottom' ? -halfStroke * signY : 0);
          // not become a vertical line?
          if (borderLeft !== borderRight) {
            top = borderTop;
            bottom = borderBottom;
          }
          // not become a horizontal line?
          if (borderTop !== borderBottom) {
            left = borderLeft;
            right = borderRight;
          }
        }

        ctx.beginPath();
        ctx.fillStyle = vm.backgroundColor;
        ctx.strokeStyle = vm.borderColor;
        ctx.lineWidth = borderWidth;

        // Corner points, from bottom-left to bottom-right clockwise
        // | 1 2 |
        // | 0 3 |
        const corners = [
          [left, bottom],
          [left, top],
          [right, top],
          [right, bottom],
        ];

        // Find first (starting) corner with fallback to 'bottom'
        const borders = ['bottom', 'left', 'top', 'right'];
        let startCorner = borders.indexOf(borderSkipped, 0);
        if (startCorner === -1) {
          startCorner = 0;
        }

        function cornerAt(index) {
          return corners[(startCorner + index) % 4];
        }

        // Draw rectangle from 'startCorner'
        let corner = cornerAt(0);
        ctx.moveTo(corner[0], corner[1]);

        for (let i = 1; i < 4; i += 1) {
          corner = cornerAt(i);
          let nextCornerId = i + 1;
          if (nextCornerId === 4) {
            nextCornerId = 0;
          }

          const width = corners[2][0] - corners[1][0];
          const height = corners[0][1] - corners[1][1];
          const x = corners[1][0];
          const y = corners[1][1];

          radius = cornerRadius;
          // Fix radius being too large
          if (radius > Math.abs(height) / 2) {
            radius = Math.floor(Math.abs(height) / 2);
          }
          if (radius > Math.abs(width) / 2) {
            radius = Math.floor(Math.abs(width) / 2);
          }

          if (height < 0) {
            // Negative values in a standard bar chart
            const xTl = x; const xTr = x + width;
            const yTl = y + height; const yTr = y + height;

            const xBl = x; const xBr = x + width;
            const yBl = y; const yBr = y;

            // Draw
            ctx.moveTo(xBl + radius, yBl);
            ctx.lineTo(xBr - radius, yBr);
            ctx.quadraticCurveTo(xBr, yBr, xBr, yBr - radius);
            ctx.lineTo(xTr, yTr + radius);
            ctx.quadraticCurveTo(xTr, yTr, xTr - radius, yTr);
            ctx.lineTo(xTl + radius, yTl);
            ctx.quadraticCurveTo(xTl, yTl, xTl, yTl + radius);
            ctx.lineTo(xBl, yBl - radius);
            ctx.quadraticCurveTo(xBl, yBl, xBl + radius, yBl);
          } else if (width < 0) {
            // Negative values in a horizontal bar chart
            const xTl = x + width; const xTr = x;
            const yTl = y; const yTr = y;

            const xBl = x + width; const xBr = x;
            const yBl = y + height; const yBr = y + height;

            // Draw
            ctx.moveTo(xBl + radius, yBl);
            ctx.lineTo(xBr - radius, yBr);
            ctx.quadraticCurveTo(xBr, yBr, xBr, yBr - radius);
            ctx.lineTo(xTr, yTr + radius);
            ctx.quadraticCurveTo(xTr, yTr, xTr - radius, yTr);
            ctx.lineTo(xTl + radius, yTl);
            ctx.quadraticCurveTo(xTl, yTl, xTl, yTl + radius);
            ctx.lineTo(xBl, yBl - radius);
            ctx.quadraticCurveTo(xBl, yBl, xBl + radius, yBl);
          } else {
            // Positive Value
            ctx.moveTo(x + radius, y);
            ctx.lineTo(x + width - radius, y);
            ctx.quadraticCurveTo(x + width, y, x + width, y + radius);
            ctx.lineTo(x + width, y + height - radius);
            ctx.quadraticCurveTo(x + width, y + height, x + width - radius, y + height);
            ctx.lineTo(x + radius, y + height);
            ctx.quadraticCurveTo(x, y + height, x, y + height - radius);
            ctx.lineTo(x, y + radius);
            ctx.quadraticCurveTo(x, y, x + radius, y);
          }
        }

        ctx.fill();
        if (borderWidth) {
          ctx.stroke();
        }
      },
    });
}

And then add a cornerRadius value to the options object, like this:

render() {
    return (
          <Bar
            data={data}
            options={{ cornerRadius: 10 }}
          />
    );
}

I'm having the same issue. I tried to apply your solution @danielnmai without success. If I place the Chart.helpers.extend inside the class I get "Parsing error: Unexpected token" on the period between Chart and helpers (I've imported Chart from react-chartjs-2).
If I place it inside a componentDidMount or the constructor the chart appears to be working but without any element, even though the tooltips are correctly showing up...
image
Sorry if this sounds like a stupid question, but I'm quite new to React. Thank you for your help!

I was able to get this to work. Make sure you include am import for the chart at the top:

import { Chart } from "react-chartjs-2";

How do you replace var ctx = this._chart.ctx; var vm = this._view; when using React hooks?

How do you replace var ctx = this._chart.ctx; var vm = this._view; when using React hooks?

doesn't seem like you need to. I was able to get it working in my functional components by simply including the code in my functional component's js

How do you replace var ctx = this._chart.ctx; var vm = this._view; when using React hooks?

doesn't seem like you need to. I was able to get it working in my functional components by simply including the code in my functional component's js


In the case of having multiple charts in a same page, would you have problem that all of them share this same config of rounded bars?

How do you replace var ctx = this._chart.ctx; var vm = this._view; when using React hooks?

doesn't seem like you need to. I was able to get it working in my functional components by simply including the code in my functional component's js

In the case of having multiple charts in a same page, would you have problem that all of them share this same config of rounded bars?

There is no problem with that, i have 3 charts using the same config with different data sets, and it works fine.

Closing as not relevant to wrapper

Was this page helpful?
0 / 5 - 0 ratings

Related issues

alexchoiweb picture alexchoiweb  路  3Comments

LuizMoreira picture LuizMoreira  路  3Comments

flavz27 picture flavz27  路  5Comments

DavidSongzw picture DavidSongzw  路  5Comments

Pringels picture Pringels  路  4Comments