[Chartjs]-How to make gap in chart.js graph?

3👍

You could accomplish that by creating a chart plugin, like so …

Chart.plugins.register({
    afterUpdate: function(chart) {
        var dataset = chart.config.data.datasets[0];
        var offset = 27;
        for (var i = 0; i < dataset.data.length; i++) {
            var model = dataset._meta[0].data[i]._model;
            model.x += offset;
            model.controlPointNextX += offset;
            model.controlPointPreviousX += offset;
        }
    }
});

ᴅᴇᴍᴏ

// register plugin
Chart.plugins.register({
    afterUpdate: function(chart) {
        var dataset = chart.config.data.datasets[0];
        var offset = 27; //set that suits the best
        for (var i = 0; i < dataset.data.length; i++) {
            var model = dataset._meta[0].data[i]._model;
            model.x += offset;
            model.controlPointNextX += offset;
            model.controlPointPreviousX += offset;
        }
    }
});

// generate chart
var ctx = document.querySelector('#canvas').getContext('2d');
var myChart = new Chart(ctx, {
    type: 'line',
    data: {
        labels: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun'],
        datasets: [{
            label: 'Line Chart',
            data: [10, 20, 18, 16, 19, 22],
            backgroundColor: 'rgba(75,192,192, 0.4)',
            borderColor: '#4bc0c0',
            pointBackgroundColor: 'black',
            tension: 0,
        }]
    },
    options: {
        responsive: false,
        scales: {
            xAxes: [{
                gridLines: {
                    offsetGridLines: true,
                    display: false
                }
            }],
            yAxes: [{
                ticks: {
                    beginAtZero: true
                },
                gridLines: {
                    display: false
                }
            }]
        }
    }
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/2.5.0/Chart.min.js"></script>
<canvas id="canvas" width="340" height="180"></canvas>

2👍

You can create a gap by specifying the min and max values of the scale.

In your case, you have X as a time scale and Y as a linear scale. Therefore each will have its own additional options.

If you determine the min and max of your data then you can add an offset to your scale range. For example [min – 123, max + 123] considering that the offset is 123.

I created bellow an Angular component which uses Chart.js. Angular component because this seems to be the requirement. Also the angular tag was placed before the question was edited. If the Angular component is not required then the important logic is found inside createChart() and most of the code is related to Chart.js and little to Angular.

Note that the following code is written in TypeScript as this is the recommended way of writing Angular apps.

Dependencies

  • Graph.js: npm install –save chart.js
  • Graph.js typings: typings install –save –global dt~chartjs
  • Moment: npm install –save moment

Chart data model

Chart data set is modeled as such:

export interface GraphDataSet {
  label: string;
  data: { x: any, y: any }[];
}

Where label is the label which describes this data set.

data is the array of x-y pairs that describe the position of X and Y axis.

Note that one chart can have multiple data sets. This model corresponds to one data set. Therefore the chart component will receive an array of data set models.

Component

import {AfterViewChecked, Component, ElementRef, Input, OnInit, ViewChild} from '@angular/core';
import 'chart.js';
import Chart from 'chart.js';
import {GraphDataSet} from './GraphDataSet';
import * as moment from 'moment';

@Component({
  selector: 'graph-line',
  template: `<canvas #canvas [width]="width" [height]="height"></canvas>`
})
export class GraphLineComponent implements OnInit, AfterViewChecked {

  private _canvas: ElementRef;
  private _dataSets: GraphDataSet[];
  private _chart: Chart;

  @Input()
  width: number;

  @Input()
  height: number;

  @Input()
  public dataSets: GraphDataSet[];

  @ViewChild('canvas')
  public canvas: ElementRef;

  constructor() {
  }

  ngOnInit() {
  }

  ngAfterViewChecked(): void {
    if (this.canvas !== this._canvas || this.dataSets !== this._dataSets) {
      this._canvas = this.canvas;
      this._dataSets = this.dataSets;
      setTimeout(() => this.createChart(), 0);
    }
  }

  private createChart() {

    const ctx = this.canvas.nativeElement.getContext('2d');

    // create background linear gradient (top-to-bottom)
    const backgroundGradient = ctx.createLinearGradient(this.width / 2, 0, this.width / 2, this.height);
    backgroundGradient.addColorStop(0, '#5ebb95');
    backgroundGradient.addColorStop(1, 'rgba(255, 255, 255, 0)');

    // create default settings that can be used to style the dataset
    const defaultDataSetSettings = {
      backgroundColor: backgroundGradient,
      borderColor: '#5ebb95',
      borderWidth: 1,
      pointBackgroundColor: '#5ebb95',
      tension: 0,
      pointBorderWidth: 7,
      pointHoverBorderWidth: 10
    };

    const data = {
      datasets: this.dataSets.map(dataSet => Object.assign({}, defaultDataSetSettings, dataSet))
    };

    const xMin = this.getXMin(this.dataSets);
    const xMax = this.getXMax(this.dataSets);
    const timeXMin = xMin.subtract(1, 'months');
    const timeXMax = xMax.add(1, 'months');

    this._chart = new Chart(ctx, {
      type: 'line',
      data: data,
      options: {
        responsive: false,
        tooltips: {
          callbacks: {
            title: (item, chartData) => item.map(i => chartData.datasets[i.datasetIndex].label),
            label: (item, chartData) => item.yLabel + ' on ' + moment(item.xLabel).format('LL')
          }
        },
        scales: {
          xAxes: [{
            type: 'time',
            position: 'bottom',
            gridLines: {
              display: false
            },
            time: {
              displayFormats: {
                month: 'MMM'
              },
              unit: 'month',
              min: timeXMin,
              max: timeXMax
            },
            ticks: {
              callback: (value, index, values) => {
                const date = values[index];
                console.log(date.diff(xMin, 'months'));
                return date.diff(xMin, 'days') <= 0 || date.diff(xMax, 'days') >= 0 ? null : value;
              }
            }
          }],
          yAxes: [{
            type: 'linear',
            position: 'left',
            gridLines: {
              borderDash: [10, 10],
              zeroLineWidth: 0
            },
            ticks: {
              min: 0,
              fixedStepSize: 200,
              callback: (value) => value === 0 ? null : value
            }
          }]
        }
      }
    });
  }

  private getXMin(dataSets: GraphDataSet[]) {
    let min: Date = null;

    dataSets.forEach(dataSet => {
      dataSet.data.forEach(data => {
        if (!min || data.x < min) {
          min = data.x;
        }
      });
    });

    return moment(min);
  }

  private getXMax(dataSets: GraphDataSet[]) {
    let max: Date = null;

    dataSets.forEach(dataSet => {
      dataSet.data.forEach(data => {
        if (!max || data.x > max) {
          max = data.x;
        }
      });
    });

    return moment(max);
  }
}

Usage

<graph-line [dataSets]="dataSets" [width]="500" [height]="300"></graph-line>

Data

this.dataSets = [
  {
    label: 'My Monthly Report',
    data: [
      {x: Date.parse('2017-01-10'), y: 500},
      {x: Date.parse('2017-02-15'), y: 700},
      {x: Date.parse('2017-03-10'), y: 600},
      {x: Date.parse('2017-04-20'), y: 590},
      {x: Date.parse('2017-05-30'), y: 610},
      {x: Date.parse('2017-06-25'), y: 800}
    ]
  }
];

Explanation

Explanation :: X Axis

      xAxes: [{
        type: 'time',
        position: 'bottom',
        gridLines: {
          display: false
        },
        time: {
          displayFormats: {
            month: 'MMM'
          },
          unit: 'month',
          min: timeXMin,
          max: timeXMax
        },
        ticks: {
          callback: (value, index, values) => {
            const date = values[index];
            console.log(date.diff(xMin, 'months'));
            return date.diff(xMin, 'days') <= 0 || date.diff(xMax, 'days') >= 0 ? null : value;
          }
        }
      }],

This is a time axis which has the range determined by timeXMin and timeXMax. These are computed by finding the min and max of all data sets. Then an offset of one month is added like so xMin.subtract(1, 'months') and xMax.add(1, 'months') respectively.

time.displayFormats.month will display months as labels.

time.unit: 'month' will split the scale into months, then each of the unit will be formated as label using time.displayFormats.month.

ticks.callback is used to remove the labels for time units that are outside of [xMin, xMax] which is the min and max of all data sets.

Explanation :: Y Axis

      yAxes: [{
        type: 'linear',
        position: 'left',
        gridLines: {
          borderDash: [10, 10],
          zeroLineWidth: 0
        },
        ticks: {
          min: 0,
          fixedStepSize: 200,
          callback: (value) => value === 0 ? null : value
        }
      }]

This is a linear scale.

gridLines.borderDash creates dash lines for each tick on the linear scale.

ticks.min: 0 specify that the scale will start at zero.

ticks.fixedStepSize: 200 specify that each scale tick will have a step of 200.

ticks.callback remove the label for 0 value.

Explanation :: Component lifecycle

Component has width and height inputs in order to set the size of the <canvas> which is the only element inside its template.

Another input is dataSets which is the actual data that is passed to Chart.js.

The component has ngAfterViewChecked() hook, which means that can check for changes like, if any input has changed or if the <canvas> element has changed for some reason. If we see that some of the inputs have changed then we create the chart like this setTimeout(() => this.createChart(), 0);. setTimeout() is used because at this time we cannot change any parameters, and to be safe, the processing is done on a separate call stack.

Explanation :: Data

this.dataSets = [
  {
    label: 'My Monthly Report',
    data: [
      {x: Date.parse('2017-01-10'), y: 500},
      {x: Date.parse('2017-02-15'), y: 700},
      {x: Date.parse('2017-03-10'), y: 600},
      {x: Date.parse('2017-04-20'), y: 590},
      {x: Date.parse('2017-05-30'), y: 610},
      {x: Date.parse('2017-06-25'), y: 800}
    ]
  }
];

Because X is a linear scale and Y is a time scale, data is formatted as { x, y }. x is a date type and y is a number. x and y properties are recognized by Chart.js and assigned to X and Y scales respectively.

Explanation :: Dataset Settings

const ctx = this.canvas.nativeElement.getContext('2d');

// create background linear gradient (top-to-bottom)
const backgroundGradient = ctx.createLinearGradient(this.width / 2, 0, this.width / 2, this.height);
backgroundGradient.addColorStop(0, '#5ebb95');
backgroundGradient.addColorStop(1, 'rgba(255, 255, 255, 0)');

// create default settings that can be used to style the dataset
const defaultDataSetSettings = {
  backgroundColor: backgroundGradient,
  borderColor: '#5ebb95',
  borderWidth: 1,
  pointBackgroundColor: '#5ebb95',
  tension: 0,
  pointBorderWidth: 7,
  pointHoverBorderWidth: 10
};

const data = {
  datasets: this.dataSets.map(dataSet => Object.assign({}, defaultDataSetSettings, dataSet))
};

The purpose of this code is to create the default settings that can be associated with a data set. The default data set settings are extended with the user defined ones like datasets: this.dataSets.map(dataSet => Object.assign({}, defaultDataSetSettings, dataSet)).

In the design the backgroundColor is a gradient, therefore a gradient which transition from top-to-bottom is created using the canvas context ctx.createLinearGradient(...).

Leave a comment