Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Timeseries scale #4364

Closed
wants to merge 7 commits into from
Closed
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/SUMMARY.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@
* [Category](axes/cartesian/category.md)
* [Linear](axes/cartesian/linear.md)
* [Logarithmic](axes/cartesian/logarithmic.md)
* [Time](axes/cartesian/time.md)
* [Time and TimeSeries](axes/cartesian/time.md)
* [Radial](axes/radial/README.md)
* [Linear](axes/radial/linear.md)
* [Labelling](axes/labelling.md)
Expand Down
19 changes: 18 additions & 1 deletion docs/axes/cartesian/time.md
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,23 @@ var chart = new Chart(ctx, {
```

## Parser
If this property is defined as a string, it is interpreted as a custom format to be used by moment to parse the date.
If this property is defined as a string, it is interpreted as a custom format to be used by moment to parse the date.

If this is a function, it must return a moment.js object given the appropriate data value.

# TimeSeries Cartesian Axis

An other option to display dates is TimeSeries scale. Unlike time scale it shows all points from all datasets with same distance by x axes.

## Configuration Options

Configuration options for timeseries scale are very similar to time scale options. The difference is that timeseries don't have tick generation options like 'min', 'max' and 'stepSize'. These options extend the [common tick configuration](README.md#tick-configuration).

| Name | Type | Default | Description
| -----| ---- | --------| -----------
| `displayFormats` | `Object` | | Sets how different time units are displayed. [more...](#display-formats)
| `parser` | `String` or `Function` | | Custom parser for dates. [more...](#parser)
| `round` | `String` | `false` | If defined, dates will be rounded to the start of this unit. See [Time Units](#scales-time-units) below for the allowed units.
| `tooltipFormat` | `String` | | The moment js format string to use for the tooltip.
| `unit` | `String` | `false` | If defined, will force the unit to be a certain type. See [Time Units](#scales-time-units) section below for details.
| `minUnit` | `String` | `millisecond` | The minimum display format to be used for a time unit.
5 changes: 4 additions & 1 deletion samples/samples.js
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@
path: 'scales/logarithmic/scatter.html'
}]
}, {
title: 'Time scale',
title: 'Time and TimeSeries scale',
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we keep title: 'Time scales' and avoid the title on 2 lines?

image

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

items: [{
title: 'Line',
path: 'scales/time/line.html'
Expand All @@ -115,6 +115,9 @@
}, {
title: 'Combo',
path: 'scales/time/combo.html'
}, {
title: 'Line (timeseries scale)',
path: 'scales/time/line-timeseries.html'
}]
}, {
title: 'Scale options',
Expand Down
167 changes: 167 additions & 0 deletions samples/scales/time/line-timeseries.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
<!doctype html>
<html>

<head>
<title>Line Chart</title>
<script src="https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.13.0/moment.min.js"></script>
<script src="../../../dist/Chart.js"></script>
<script src="../../utils.js"></script>
<style>
canvas {
-moz-user-select: none;
-webkit-user-select: none;
-ms-user-select: none;
}
</style>
</head>

<body>
<div style="width:75%;">
<canvas id="canvas"></canvas>
</div>
<br>
<br>
<button id="randomizeData">Randomize Data</button>
<button id="addDataset">Add Dataset</button>
<button id="removeDataset">Remove Dataset</button>
<button id="addData">Add Data</button>
<button id="removeData">Remove Data</button>
<script>
var timeFormat = 'MM/DD/YYYY HH:mm';

function newDate(days) {
return moment().add(days, 'd').toDate();
}

function newDateString(days) {
return moment().add(days, 'd').format();
}

function newTimestamp(days) {
return moment().add(days, 'd').unix();
}

var numDataPoints = 120;
var labels = []
var data = []
for (var i = 0; i < numDataPoints; i++) {
labels.push(newDate(i));
data.push(randomScalingFactor());
}

var color = Chart.helpers.color;
var config = {
type: 'line',
data: {
labels: labels,
datasets: [{
label: "My First dataset",
backgroundColor: color(window.chartColors.red).alpha(0.5).rgbString(),
borderColor: window.chartColors.red,
fill: false,
data: data,
}]
},
options: {
title:{
text: "Chart.js Time Scale"
},
scales: {
xAxes: [{
type: "timeseries",
time: {
format: timeFormat,
// round: 'day'
tooltipFormat: 'll HH:mm'
},
scaleLabel: {
display: true,
labelString: 'Date'
}
}],
yAxes: [{
scaleLabel: {
display: true,
labelString: 'value'
}
}]
},
}
};

window.onload = function() {
var ctx = document.getElementById("canvas").getContext("2d");
window.myLine = new Chart(ctx, config);

};

document.getElementById('randomizeData').addEventListener('click', function() {
config.data.datasets.forEach(function(dataset) {
dataset.data.forEach(function(dataObj, j) {
if (typeof dataObj === 'object') {
dataObj.y = randomScalingFactor();
} else {
dataset.data[j] = randomScalingFactor();
}
});
});

window.myLine.update();
});

var colorNames = Object.keys(window.chartColors);
document.getElementById('addDataset').addEventListener('click', function() {
var colorName = colorNames[config.data.datasets.length % colorNames.length];
var newColor = window.chartColors[colorName]
var newDataset = {
label: 'Dataset ' + config.data.datasets.length,
borderColor: newColor,
backgroundColor: color(newColor).alpha(0.5).rgbString(),
data: [],
};

for (var index = 0; index < config.data.labels.length; ++index) {
newDataset.data.push(randomScalingFactor());
}

config.data.datasets.push(newDataset);
window.myLine.update();
});

document.getElementById('addData').addEventListener('click', function() {
if (config.data.datasets.length > 0) {
config.data.labels.push(newDate(config.data.labels.length));

for (var index = 0; index < config.data.datasets.length; ++index) {
if (typeof config.data.datasets[index].data[0] === "object") {
config.data.datasets[index].data.push({
x: newDate(config.data.datasets[index].data.length),
y: randomScalingFactor(),
});
} else {
config.data.datasets[index].data.push(randomScalingFactor());
}
}

window.myLine.update();
}
});

document.getElementById('removeDataset').addEventListener('click', function() {
config.data.datasets.splice(0, 1);
window.myLine.update();
});

document.getElementById('removeData').addEventListener('click', function() {
config.data.labels.splice(-1, 1); // remove the label first

config.data.datasets.forEach(function(dataset, datasetIndex) {
dataset.data.pop();
});

window.myLine.update();
});
</script>
</body>

</html>
2 changes: 2 additions & 0 deletions src/chart.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,11 +28,13 @@ require('./elements/element.point')(Chart);
require('./elements/element.rectangle')(Chart);

require('./scales/scale.linearbase')(Chart);
require('./scales/scale.timebase')(Chart);
require('./scales/scale.category')(Chart);
require('./scales/scale.linear')(Chart);
require('./scales/scale.logarithmic')(Chart);
require('./scales/scale.radialLinear')(Chart);
require('./scales/scale.time')(Chart);
require('./scales/scale.timeseries')(Chart);

// Controllers must be loaded after elements
// See Chart.core.datasetController.dataElementType
Expand Down
1 change: 0 additions & 1 deletion src/helpers/helpers.time.js
Original file line number Diff line number Diff line change
Expand Up @@ -219,7 +219,6 @@ module.exports = function(Chart) {
max: niceMax
});
}

};

};
87 changes: 7 additions & 80 deletions src/scales/scale.time.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ var moment = require('moment');
moment = typeof(moment) === 'function' ? moment : window.moment;

module.exports = function(Chart) {

var helpers = Chart.helpers;
var timeHelpers = helpers.time;

Expand Down Expand Up @@ -39,7 +38,7 @@ module.exports = function(Chart) {
}
};

var TimeScale = Chart.Scale.extend({
var TimeScale = Chart.TimeScaleBase.extend({
initialize: function() {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this initialize override needs to be removed (looks exactly the same as the one in TimeScaleBase).

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yep

if (!moment) {
throw new Error('Chart.js - Moment.js could not be found! You must include it before Chart.js to use the time scale. Download at https://momentjs.com');
Expand All @@ -49,6 +48,7 @@ module.exports = function(Chart) {

Chart.Scale.prototype.initialize.call(this);
},

determineDataLimits: function() {
var me = this;
var timeOpts = me.options.time;
Expand Down Expand Up @@ -115,6 +115,7 @@ module.exports = function(Chart) {
me.dataMax = dataMax;
me._parsedData = parsedData;
},

buildTicks: function() {
var me = this;
var timeOpts = me.options.time;
Expand Down Expand Up @@ -166,61 +167,7 @@ module.exports = function(Chart) {
me.max = helpers.max(me.ticks);
me.min = helpers.min(me.ticks);
},
// Get tooltip label
getLabelForIndex: function(index, datasetIndex) {
var me = this;
var label = me.chart.data.labels && index < me.chart.data.labels.length ? me.chart.data.labels[index] : '';
var value = me.chart.data.datasets[datasetIndex].data[index];

if (value !== null && typeof value === 'object') {
label = me.getRightValue(value);
}

// Format nicely
if (me.options.time.tooltipFormat) {
label = timeHelpers.parseTime(me, label).format(me.options.time.tooltipFormat);
}

return label;
},
// Function to format an individual tick mark
tickFormatFunction: function(tick, index, ticks) {
var formattedTick;
var tickClone = tick.clone();
var tickTimestamp = tick.valueOf();
var major = false;
var tickOpts;
if (this.majorUnit && this.majorDisplayFormat && tickTimestamp === tickClone.startOf(this.majorUnit).valueOf()) {
// format as major unit
formattedTick = tick.format(this.majorDisplayFormat);
tickOpts = this.options.ticks.major;
major = true;
} else {
// format as minor (base) unit
formattedTick = tick.format(this.displayFormat);
tickOpts = this.options.ticks.minor;
}

var callback = helpers.valueOrDefault(tickOpts.callback, tickOpts.userCallback);

if (callback) {
return {
value: callback(formattedTick, index, ticks),
major: major
};
}
return {
value: formattedTick,
major: major
};
},
convertTicksToLabels: function() {
var me = this;
me.ticksAsTimestamps = me.ticks;
me.ticks = me.ticks.map(function(tick) {
return moment(tick);
}).map(me.tickFormatFunction, me);
},
getPixelForOffset: function(offset) {
var me = this;
var epochWidth = me.max - me.min;
Expand All @@ -234,6 +181,7 @@ module.exports = function(Chart) {
var heightOffset = (me.height * decimal);
return me.top + Math.round(heightOffset);
},

getPixelForValue: function(value, index, datasetIndex) {
var me = this;
var offset = null;
Expand All @@ -256,39 +204,18 @@ module.exports = function(Chart) {
return me.getPixelForOffset(offset);
}
},

getPixelForTick: function(index) {
return this.getPixelForOffset(this.ticksAsTimestamps[index]);
},

getValueForPixel: function(pixel) {
var me = this;
var innerDimension = me.isHorizontal() ? me.width : me.height;
var offset = (pixel - (me.isHorizontal() ? me.left : me.top)) / innerDimension;
return moment(me.min + (offset * (me.max - me.min)));
},
// Crude approximation of what the label width might be
getLabelWidth: function(label) {
var me = this;
var ticks = me.options.ticks;

var tickLabelWidth = me.ctx.measureText(label).width;
var cosRotation = Math.cos(helpers.toRadians(ticks.maxRotation));
var sinRotation = Math.sin(helpers.toRadians(ticks.maxRotation));
var tickFontSize = helpers.valueOrDefault(ticks.fontSize, Chart.defaults.global.defaultFontSize);
return (tickLabelWidth * cosRotation) + (tickFontSize * sinRotation);
},
getLabelCapacity: function(exampleTime) {
var me = this;

me.displayFormat = me.options.time.displayFormats.millisecond; // Pick the longest format for guestimation
var exampleLabel = me.tickFormatFunction(moment(exampleTime), 0, []).value;
var tickLabelWidth = me.getLabelWidth(exampleLabel);

var innerWidth = me.isHorizontal() ? me.width : me.height;
var labelCapacity = innerWidth / tickLabelWidth;

return labelCapacity;
}
});
Chart.scaleService.registerScaleType('time', TimeScale, defaultConfig);

Chart.scaleService.registerScaleType('time', TimeScale, defaultConfig);
};
Loading