Chart.js icon indicating copy to clipboard operation
Chart.js copied to clipboard

Allow dashed bar borders

Open perkalerk opened this issue 6 years ago • 13 comments
trafficstars

I'm trying to make dashed borders for the bar chart, but having trouble with 2.8. I've found code online that has working examples for 2.1: http://jsfiddle.net/Ls8u10dp/ but the same thing doesn't work with 2.8: http://jsfiddle.net/L7ubzshc/

Is there any way around this?

Thanks

perkalerk avatar Nov 04 '19 21:11 perkalerk

@perkalerk border dash is not currently available for BAR chart. Maybe you could ask for an enhancement or prepare a PR.

In version 2.9.2, as far as I have seen, the elements are changed into ES6 objects and therefore the examples does not work anymore. But let me say I'm not really a JS developer and maybe someone else can find another workaround.

But waiting for enhancement, you can use a plugin to do what you need.

I've prepared an example (you should extend it if you have other requiremets) and it should work (tested in your jsfiddle link). Here you can see the whole Javascript part:

var config = {
  type: 'bar',
  data: {
    labels: ["January", "February", "March", "April", "May", "June", "July"],
    datasets: [{
      label: "My First dataset",
      backgroundColor: "rgba(255,99,132,0.2)",
      data: [65, 59, 80, 81, 56, 45, 40],
      borderColor: 'blue',
      borderWidth: 0,
    }]
  },
  plugins: [{
   afterDatasetDraw: function(chart, args, options) {
      args.meta.data.forEach(function(element) {
        var borderWidth = 3;
        var ctx = chart.ctx;
        var vm = element._view;
        var half = vm.width / 2;
        var left = vm.x - half;
        var right = vm.x + half;
        var top = vm.y;
        var width = right - left;
        var height = chart.chartArea.bottom - top + (borderWidth / 2) - 1;
        ctx.beginPath();
        ctx.lineWidth = borderWidth;
        ctx.strokeStyle = vm.borderColor;
        ctx.setLineDash([5, 5]);
        ctx.moveTo(left, top);
        ctx.lineTo(left, top + height);
        ctx.moveTo(left, top);
        ctx.lineTo(left + width, top);
        ctx.moveTo(left + width, top);
        ctx.lineTo(left + width, top + height);
        ctx.stroke();
        ctx.save();
	});
    }
  }]
};

var ctx = document.getElementById("myChart").getContext("2d");
myChart = new Chart(ctx, config);

myBug

A couple of point to pay attention:

  1. in chart options set borderWidth: 0, and set your border width by plugin
  2. The legend does not reflect the border width and dash you set. Maybe by legend label callback you can fix it.

Again, just a workaround taht does not address all cases. Apologize if my javascript code is not good!

stockiNail avatar Nov 08 '19 18:11 stockiNail

In version 2.9.2, as far as I have seen, the elements are changed into ES6 objects

That's only in 3.0 (yet to be released)

benmccann avatar Nov 08 '19 20:11 benmccann

@benmccann sorry my fault. I was convinced to be written as comment.

stockiNail avatar Nov 08 '19 21:11 stockiNail

@perkalerk about why the online code does not work anymore in version 2.8, in this version the Rectangle is drawn without using any line (or stroke) therefore the setLineDash context method does not effect on the the rectangle.

stockiNail avatar Nov 12 '19 08:11 stockiNail

@perkalerk border dash is not currently available for BAR chart. Maybe you could ask for an enhancement or prepare a PR.

In version 2.9.2, as far as I have seen, the elements are changed into ES6 objects and therefore the examples does not work anymore. But let me say I'm not really a JS developer and maybe someone else can find another workaround.

But waiting for enhancement, you can use a plugin to do what you need.

I've prepared an example (you should extend it if you have other requiremets) and it should work (tested in your jsfiddle link). Here you can see the whole Javascript part:

var config = {
  type: 'bar',
  data: {
    labels: ["January", "February", "March", "April", "May", "June", "July"],
    datasets: [{
      label: "My First dataset",
      backgroundColor: "rgba(255,99,132,0.2)",
      data: [65, 59, 80, 81, 56, 45, 40],
      borderColor: 'blue',
      borderWidth: 0,
    }]
  },
  plugins: [{
   afterDatasetDraw: function(chart, args, options) {
      args.meta.data.forEach(function(element) {
        var borderWidth = 3;
        var ctx = chart.ctx;
        var vm = element._view;
        var half = vm.width / 2;
        var left = vm.x - half;
        var right = vm.x + half;
        var top = vm.y;
        var width = right - left;
        var height = chart.chartArea.bottom - top + (borderWidth / 2) - 1;
        ctx.beginPath();
        ctx.lineWidth = borderWidth;
        ctx.strokeStyle = vm.borderColor;
        ctx.setLineDash([5, 5]);
        ctx.moveTo(left, top);
        ctx.lineTo(left, top + height);
        ctx.moveTo(left, top);
        ctx.lineTo(left + width, top);
        ctx.moveTo(left + width, top);
        ctx.lineTo(left + width, top + height);
        ctx.stroke();
        ctx.save();
	});
    }
  }]
};

var ctx = document.getElementById("myChart").getContext("2d");
myChart = new Chart(ctx, config);

myBug

A couple of point to pay attention:

  1. in chart options set borderWidth: 0, and set your border width by plugin
  2. The legend does not reflect the border width and dash you set. Maybe by legend label callback you can fix it.

Again, just a workaround taht does not address all cases. Apologize if my javascript code is not good!

Hi, Your code Is good and working with latest version for bar chart. do you have code for horizontal bar chart ?

bhaveshpatel200 avatar Jan 11 '20 07:01 bhaveshpatel200

Hi, below code is working for 'Horizontal Bar'.

var config = {
  type: 'horizontalBar',
  scales:{
  xAxes:[{position : 'top', ticks: {beginAtZero: true, min: 10}}],
  yAxes:[{}]
  },
  data: {
    labels: ["January", "February", "March", "April", "May", "June", "July"],
    datasets: [{
      label: "My First dataset",
      backgroundColor: "rgba(255,99,132,0.2)",
      data: [65, 59, 80, 81, 56, 45, 40],
      borderColor: 'blue',
      borderWidth: 0,
    }]
  },
  plugins: [{
   afterDatasetDraw: function(chart, args, options) {
      args.meta.data.forEach(function(element) {
        var borderWidth = 3;
        var ctx = chart.ctx;
        var vm = element._view;        
      
      	var half = vm.height / 2;
        var top = vm.y - half;
        var bottom = vm.y + half;
        var right = vm.x;
        var left = chart.chartArea.left;
        
        ctx.beginPath();
        ctx.lineWidth = borderWidth;
        ctx.strokeStyle = vm.borderColor;
        ctx.setLineDash([6, 6]);
        
        ctx.moveTo(left, top);
        ctx.lineTo(right, top);
        ctx.moveTo(right, top);
        ctx.lineTo(right, bottom);
        ctx.moveTo(right, bottom);
        ctx.lineTo(left, bottom);
        
        ctx.stroke();
        ctx.save();
	});
    }
  }]
};

var ctx = document.getElementById("myChart").getContext("2d");
myChart = new Chart(ctx, config);

bhaveshpatel200 avatar Jan 11 '20 09:01 bhaveshpatel200

Here is some example how it can be done (extension of the BarElement element) Could you implement this?

class BarElement extends Element {
  constructor(cfg) {
    super();
    this.options = undefined;
    this.horizontal = undefined;
    this.base = undefined;
    this.width = undefined;
    this.height = undefined;
    this.inflateAmount = undefined;
    if (cfg) {
      Object.assign(this, cfg);
    }
  }
  draw(ctx) {
      //------------------------------changed for OT---------------------------------------------
    const {inflateAmount, options: {borderColor, backgroundColor, borderDash}} = this;
      //------------------------------end---------------------------------------------
    const {inner, outer} = boundingRects(this);
    const addRectPath = hasRadius(outer.radius) ? addRoundedRectPath : addNormalRectPath;
    ctx.save();
    if (outer.w !== inner.w || outer.h !== inner.h) {
      ctx.beginPath();
      addRectPath(ctx, inflateRect(outer, inflateAmount, inner));
      ctx.clip();
      addRectPath(ctx, inflateRect(inner, -inflateAmount, outer));
      ctx.fillStyle = borderColor;
      ctx.fill('evenodd');
    }
    ctx.beginPath();
    addRectPath(ctx, inflateRect(inner, inflateAmount));
    ctx.fillStyle = backgroundColor;
    ctx.fill();
      //------------------------------changed for OT---------------------------------------------
    if (borderDash && borderDash.length > 0) {
        ctx.setLineDash(borderDash);
        ctx.strokeStyle = borderColor;
        ctx.stroke();
    }
      //------------------------------end---------------------------------------------
    ctx.restore();
  }
  inRange(mouseX, mouseY, useFinalPosition) {
    return inRange(this, mouseX, mouseY, useFinalPosition);
  }
  inXRange(mouseX, useFinalPosition) {
    return inRange(this, mouseX, null, useFinalPosition);
  }
  inYRange(mouseY, useFinalPosition) {
    return inRange(this, null, mouseY, useFinalPosition);
  }
  getCenterPoint(useFinalPosition) {
    const {x, y, base, horizontal} = this.getProps(['x', 'y', 'base', 'horizontal'], useFinalPosition);
    return {
      x: horizontal ? (x + base) / 2 : x,
      y: horizontal ? y : (y + base) / 2
    };
  }
  getRange(axis) {
    return axis === 'x' ? this.width / 2 : this.height / 2;
  }
}
BarElement.id = 'bar';
BarElement.defaults = {
  borderSkipped: 'start',
  borderWidth: 0,
  borderRadius: 0,
    //------------------------------changed for OT---------------------------------------------
    borderDash: [],
    //------------------------------end---------------------------------------------
  inflateAmount: 'auto',
  pointStyle: undefined
};
BarElement.defaultRoutes = {
  backgroundColor: 'backgroundColor',
  borderColor: 'borderColor'
};
    //------------------------------changed for OT---------------------------------------------
    BarElement.descriptors = {
        _indexable: (name) => name !== 'borderDash'
    }
    //------------------------------end---------------------------------------------

RieseIch avatar Feb 18 '22 09:02 RieseIch

Has someone made a plugin for a single bar to have dotted lines and not all of them?

sebiglesias avatar Oct 18 '22 21:10 sebiglesias

@sebiglesias you could possibly target a specific bar using the element._index property or any of the other properties in that element.

I'm currently using element._datasetIndex and element._view.backgroundColor to target the bars I want bordered.

Something like this

var config = {
  type: 'bar',
  data: {
    labels: ["January", "February", "March", "April", "May", "June", "July"],
    datasets: [{
      label: "My First dataset",
      backgroundColor: "rgba(255,99,132,0.2)",
      data: [65, 59, 80, 81, 56, 45, 40],
      borderColor: 'blue',
      borderWidth: 0,
    }]
  },
  plugins: [{
   afterDatasetDraw: function(chart, args, options) {
      args.meta.data.forEach(function(element) {
        var borderWidth = 3;
        var ctx = chart.ctx;
        var vm = element._view;
        var half = vm.width / 2;
        var left = vm.x - half;
        var right = vm.x + half;
        var top = vm.y;
        var width = right - left;
        var height = chart.chartArea.bottom - top + (borderWidth / 2) - 1;

       // Prevent beginPath from occurring depending on the index
       if (element._datasetIndex !== 1 && element._index !== 0 && vm.borderColor !== "#000") return;

        ctx.beginPath();
        ctx.lineWidth = borderWidth;
        ctx.strokeStyle = vm.borderColor;
        ctx.setLineDash([5, 5]);
        ctx.moveTo(left, top);
        ctx.lineTo(left, top + height);
        ctx.moveTo(left, top);
        ctx.lineTo(left + width, top);
        ctx.moveTo(left + width, top);
        ctx.lineTo(left + width, top + height);
        ctx.stroke();
        ctx.save();
	});
    }
  }]
};

var ctx = document.getElementById("myChart").getContext("2d");
myChart = new Chart(ctx, config);

benzend avatar Nov 11 '22 03:11 benzend

Hi, @stockiNail Thank you for the wonderful solution, I have a problem with overlapping, stacked bars are overlapping, can you please check this thread on StackOverflow? thread

LodiLofa avatar Dec 30 '22 08:12 LodiLofa

@LodiLofa the issue is in the height calculation because it was thought for no-stacked bar.:

var height = chart.chartArea.bottom - top + (borderWidth / 2) - 1;

As you can see, the height is calculated assuming to go to the bottom of chart area. You can try the following, overriding the above row:

const valueTop = element._yScale.getValueForPixel(top);
const value = chart.data.datasets[element._datasetIndex].data[element._index];
const bottom = element._yScale.getPixelForValue(valueTop - value);
var height = bottom - top + (borderWidth / 2) - 1;

codepen: https://codepen.io/stockinail/pen/ExpKEPK

stockiNail avatar Dec 30 '22 09:12 stockiNail

@LodiLofa the issue is in the height calculation because it was thought for no-stacked bar.:

var height = chart.chartArea.bottom - top + (borderWidth / 2) - 1;

As you can see, the height is calculated assuming to go to the bottom of chart area. You can try the following, overriding the above row:

const valueTop = element._yScale.getValueForPixel(top);
const value = chart.data.datasets[element._datasetIndex].data[element._index];
const bottom = element._yScale.getPixelForValue(valueTop - value);
var height = bottom - top + (borderWidth / 2) - 1;

codepen: https://codepen.io/stockinail/pen/ExpKEPK

Thank you so much your amazing bro

LodiLofa avatar Dec 30 '22 11:12 LodiLofa

I had some different requirements:

  • Rounded top corners (but only the top bars in a stacked bar chart)
  • Loose Typescript
  • Inside border
  • Supporting scripted border width (depending on which data)

Since I didn't need it / wouldn't test it I removed code for:

  • Horizontal bars
  • Skipping borders
function getRect(element: any, borderWidth: number) {
  const half = element.width / 2;
  return {
    x: element.x - half + borderWidth / 2,
    y: Math.min(element.y, element.base) + borderWidth / 2,
    width: element.width - borderWidth,
    height: Math.abs(element.base - element.y) - borderWidth,
  };
}

function getRadii(element: any) {
  const { enableBorderRadius, borderSkipped, options } = element;
  const { borderRadius } = options;
  if (!enableBorderRadius) return 0;
  return [
    borderSkipped.top ? 0 : borderRadius,
    borderSkipped.top ? 0 : borderRadius,
    borderSkipped.bottom ? 0 : borderRadius,
    borderSkipped.bottom ? 0 : borderRadius,
  ];
}

export default {
  id: "DashedBorder",
  afterDatasetDraw(chart: any, args: any) {
    const context = chart.getContext();
    const meta = args.meta;
    const dataset = meta.controller.getDataset();
    if (!dataset) return;
    const { dashedBorderDash, dashedBorderWidth } = dataset;
    if (!dashedBorderDash?.length || !dashedBorderWidth) return;
    meta.data.forEach((element: any, dataIndex: number) => {
      const fullContext = {
        ...context,
        dataset,
        dataIndex,
      };
      const borderWidth =
        dashedBorderWidth instanceof Function
          ? dashedBorderWidth(fullContext, element)
          : dashedBorderWidth;
      if (borderWidth === 0) return;

      const ctx = chart.ctx;
      const { x, y, width, height } = getRect(element, borderWidth);
      const radii = getRadii(element);
      ctx.beginPath();
      ctx.lineWidth = borderWidth;
      ctx.strokeStyle = element.options.borderColor;
      ctx.setLineDash(dashedBorderDash);
      ctx.roundRect(x, y, width, height, radii);
      ctx.stroke();
      ctx.save();
    });
  },
};

peteruithoven avatar Oct 31 '23 13:10 peteruithoven