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(...)
.