Merge remote-tracking branch 'upstream/v2.0-dev' into feature/resize_problems

Conflicts:
	gulpfile.js
This commit is contained in:
Evert Timberg 2015-09-18 18:38:47 -04:00
commit bcbd9a85e6
11 changed files with 3107 additions and 15 deletions

View File

@ -34,7 +34,8 @@ var srcFiles = [
'./src/elements/**',
'./src/charts/**',
'./node_modules/color/dist/color.min.js',
'./node_modules/javascript-detect-element-resize/detect-element-resize.js'
'./node_modules/javascript-detect-element-resize/detect-element-resize.js',
'./node_modules/moment/min/moment.min.js'
];
@ -177,7 +178,10 @@ function moduleSizesTask() {
}
function watchTask() {
gulp.watch('./src/**', ['build', 'unittest', 'unittestWatch']);
if (util.env.test) {
return gulp.watch('./src/**', ['build', 'unittest', 'unittestWatch']);
}
return gulp.watch('./src/**', ['build']);
}
function serverTask() {

View File

@ -32,6 +32,7 @@
"karma-firefox-launcher": "^0.1.6",
"karma-jasmine": "^0.3.6",
"karma-jasmine-html-reporter": "^0.1.8",
"moment": "^2.10.6",
"onecolor": "^2.5.0",
"semver": "^3.0.1"
},

View File

@ -0,0 +1,154 @@
<!doctype html>
<html>
<head>
<title>Line Chart</title>
<script src="../Chart.js"></script>
<script src="../node_modules/jquery/dist/jquery.min.js"></script>
<style>
canvas {
-webkit-box-shadow: 0 0 20px 0 rgba(0, 0, 0, .5);
}
</style>
</head>
<body>
<div style="width:50%;">
<canvas id="canvas" style="width:100%;height:100%"></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>
<div>
<h3>Legend</h3>
<div id="legendContainer">
</div>
</div>
<script>
var randomScalingFactor = function() {
return Math.ceil(Math.random() * 10.0) * Math.pow(10, Math.ceil(Math.random() * 5));
};
var randomColorFactor = function() {
return Math.round(Math.random() * 255);
};
var randomColor = function(opacity) {
return 'rgba(' + randomColorFactor() + ',' + randomColorFactor() + ',' + randomColorFactor() + ',' + (opacity || '.3') + ')';
};
var config = {
type: 'line',
data: {
labels: ["January", "February", "March", "April", "May", "June", "July"],
datasets: [{
label: "My First dataset",
data: [randomScalingFactor(), randomScalingFactor(), randomScalingFactor(), randomScalingFactor(), randomScalingFactor(), randomScalingFactor(), randomScalingFactor()],
fill: false,
borderDash: [5, 5],
}, {
label: "My Second dataset",
data: [randomScalingFactor(), randomScalingFactor(), randomScalingFactor(), randomScalingFactor(), randomScalingFactor(), randomScalingFactor(), randomScalingFactor()],
}]
},
options: {
responsive: true,
scales: {
xAxes: [{
display: true
}],
yAxes: [{
display: true,
type: 'logarithmic'
}]
}
}
};
$.each(config.data.datasets, function(i, dataset) {
dataset.borderColor = randomColor(0.4);
dataset.backgroundColor = randomColor(0.5);
dataset.pointBorderColor = randomColor(0.7);
dataset.pointBackgroundColor = randomColor(0.5);
dataset.pointBorderWidth = 1;
});
console.log(config.data);
window.onload = function() {
var ctx = document.getElementById("canvas").getContext("2d");
window.myLine = new Chart(ctx, config);
updateLegend();
};
function updateLegend() {
$legendContainer = $('#legendContainer');
$legendContainer.empty();
$legendContainer.append(window.myLine.generateLegend());
}
$('#randomizeData').click(function() {
$.each(config.data.datasets, function(i, dataset) {
dataset.data = dataset.data.map(function() {
return randomScalingFactor();
});
});
window.myLine.update();
updateLegend();
});
$('#addDataset').click(function() {
var newDataset = {
label: 'Dataset ' + config.data.datasets.length,
borderColor: randomColor(0.4),
backgroundColor: randomColor(0.5),
pointBorderColor: randomColor(0.7),
pointBackgroundColor: randomColor(0.5),
pointBorderWidth: 1,
data: [],
};
for (var index = 0; index < config.data.labels.length; ++index) {
newDataset.data.push(randomScalingFactor());
}
window.myLine.addDataset(newDataset);
updateLegend();
});
$('#addData').click(function() {
if (config.data.datasets.length > 0) {
config.data.labels.push('dataset #' + config.data.labels.length);
for (var index = 0; index < config.data.datasets.length; ++index) {
window.myLine.addData(randomScalingFactor(), index);
}
updateLegend();
}
});
$('#removeDataset').click(function() {
window.myLine.removeDataset(0);
updateLegend();
});
$('#removeData').click(function() {
config.data.labels.splice(-1, 1); // remove the label first
config.data.datasets.forEach(function(dataset, datasetIndex) {
window.myLine.removeData(datasetIndex, -1);
});
updateLegend();
});
</script>
</body>
</html>

View File

@ -0,0 +1,177 @@
<!doctype html>
<html>
<head>
<title>Line Chart</title>
<script src="../Chart.js"></script>
<script src="../node_modules/jquery/dist/jquery.min.js"></script>
<style>
canvas {
-webkit-box-shadow: 0 0 20px 0 rgba(0, 0, 0, .5);
}
</style>
</head>
<body>
<div style="width:100%;">
<canvas id="canvas" style="width:100%;height:100%"></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>
<div>
<h3>Legend</h3>
<div id="legendContainer">
</div>
</div>
<script>
var randomScalingFactor = function() {
return Math.round(Math.random() * 100 * (Math.random() > 0.5 ? -1 : 1));
};
var randomColorFactor = function() {
return Math.round(Math.random() * 255);
};
var randomColor = function(opacity) {
return 'rgba(' + randomColorFactor() + ',' + randomColorFactor() + ',' + randomColorFactor() + ',' + (opacity || '.3') + ')';
};
var newDate = function(days) {
var date = new Date();
return date.setDate(date.getDate() + days);
};
var newTimestamp = function(days) {
return Date.now() - days * 100000;
};
var config = {
type: 'line',
data: {
//labels: [newTimestamp(0), newTimestamp(1), newTimestamp(2), newTimestamp(3), newTimestamp(4), newTimestamp(5), newTimestamp(6)], // unix timestamps
// labels: [newDate(0), newDate(1), newDate(2), newDate(3), newDate(4), newDate(5), newDate(6)], // Date Objects
labels: ["01/01/2015 20:00", "01/02/2015 21:00", "01/03/2015 22:00", "01/05/2015 23:00", "01/07/2015 03:00", "01/08/2015 10:00", "02/1/2015"], // Hours
// labels: ["01/01/2015", "01/02/2015", "01/03/2015", "01/06/2015", "01/15/2015", "01/17/2015", "01/30/2015"], // Days
// labels: ["12/25/2014", "01/08/2015", "01/15/2015", "01/22/2015", "01/29/2015", "02/05/2015", "02/12/2015"], // Weeks
datasets: [{
label: "My First dataset",
data: [randomScalingFactor(), randomScalingFactor(), randomScalingFactor(), randomScalingFactor(), randomScalingFactor(), randomScalingFactor(), randomScalingFactor()],
fill: false,
borderDash: [5, 5],
}, {
label: "My Second dataset",
data: [randomScalingFactor(), randomScalingFactor(), randomScalingFactor(), randomScalingFactor(), randomScalingFactor(), randomScalingFactor(), randomScalingFactor()],
}]
},
options: {
responsive: true,
scales: {
xAxes: [{
type: "time",
display: true,
tick: {
format: 'MM/DD/YYYY HH:mm',
// round: 'day'
}
}, ],
yAxes: [{
display: true
}]
},
elements: {
line: {
tension: 0
}
},
}
};
$.each(config.data.datasets, function(i, dataset) {
dataset.borderColor = randomColor(0.4);
dataset.backgroundColor = randomColor(0.5);
dataset.pointBorderColor = randomColor(0.7);
dataset.pointBackgroundColor = randomColor(0.5);
dataset.pointBorderWidth = 1;
});
console.log(config.data);
window.onload = function() {
var ctx = document.getElementById("canvas").getContext("2d");
window.myLine = new Chart(ctx, config);
updateLegend();
};
function updateLegend() {
$legendContainer = $('#legendContainer');
$legendContainer.empty();
$legendContainer.append(window.myLine.generateLegend());
}
$('#randomizeData').click(function() {
$.each(config.data.datasets, function(i, dataset) {
dataset.data = dataset.data.map(function() {
return randomScalingFactor();
});
});
window.myLine.update();
updateLegend();
});
$('#addDataset').click(function() {
var newDataset = {
label: 'Dataset ' + config.data.datasets.length,
borderColor: randomColor(0.4),
backgroundColor: randomColor(0.5),
pointBorderColor: randomColor(0.7),
pointBackgroundColor: randomColor(0.5),
pointBorderWidth: 1,
data: [],
};
for (var index = 0; index < config.data.labels.length; ++index) {
newDataset.data.push(randomScalingFactor());
}
window.myLine.addDataset(newDataset);
updateLegend();
});
$('#addData').click(function() {
if (config.data.datasets.length > 0) {
config.data.labels.push(
moment(
config.data.labels[config.data.labels.length - 1], config.options.scales.xAxes[0].time.format
).add(1, 'day')
.format('MM/DD/YYYY')
);
for (var index = 0; index < config.data.datasets.length; ++index) {
window.myLine.addData(randomScalingFactor(), index);
}
updateLegend();
}
});
$('#removeDataset').click(function() {
window.myLine.removeDataset(0);
updateLegend();
});
$('#removeData').click(function() {
config.data.labels.splice(-1, 1); // remove the label first
config.data.datasets.forEach(function(dataset, datasetIndex) {
window.myLine.removeData(datasetIndex, -1);
});
updateLegend();
});
</script>
</body>
</html>

166
samples/scatter-logX.html Normal file
View File

@ -0,0 +1,166 @@
<!doctype html>
<html>
<head>
<title>Scatter Chart</title>
<script src="../Chart.js"></script>
<script src="../node_modules/jquery/dist/jquery.min.js"></script>
</head>
<body>
<div style="width:50%">
<div>
<canvas id="canvas" height="450" width="600"></canvas>
</div>
</div>
<script>
var randomScalingFactor = function() {
return Math.round(Math.random() * 10.0) * Math.pow(10, Math.ceil(Math.random() * 5));
};
var randomColor = function(opacity) {
return 'rgba(' + Math.round(Math.random() * 255) + ',' + Math.round(Math.random() * 255) + ',' + Math.round(Math.random() * 255) + ',' + (opacity || '.3') + ')';
};
var scatterChartData = {
datasets: [{
label: "V(node2)",
data: [{
x: 1,
y: -1.711e-2,
}, {
x: 1.26,
y: -2.708e-2,
}, {
x: 1.58,
y: -4.285e-2,
}, {
x: 2.0,
y: -6.772e-2,
}, {
x: 2.51,
y: -1.068e-1,
}, {
x: 3.16,
y: -1.681e-1,
}, {
x: 3.98,
y: -2.635e-1,
}, {
x: 5.01,
y: -4.106e-1,
}, {
x: 6.31,
y: -6.339e-1,
}, {
x: 7.94,
y: -9.659e-1,
}, {
x: 10.00,
y: -1.445,
}, {
x: 12.6,
y: -2.110,
}, {
x: 15.8,
y: -2.992,
}, {
x: 20.0,
y: -4.102,
}, {
x: 25.1,
y: -5.429,
}, {
x: 31.6,
y: -6.944,
}, {
x: 39.8,
y: -8.607,
}, {
x: 50.1,
y: -1.038e1,
}, {
x: 63.1,
y: -1.223e1,
}, {
x: 79.4,
y: -1.413e1,
}, {
x: 100.00,
y: -1.607e1,
}, {
x: 126,
y: -1.803e1,
}, {
x: 158,
y: -2e1,
}, {
x: 200,
y: -2.199e1,
}, {
x: 251,
y: -2.398e1,
}, {
x: 316,
y: -2.597e1,
}, {
x: 398,
y: -2.797e1,
}, {
x: 501,
y: -2.996e1,
}, {
x: 631,
y: -3.196e1,
}, {
x: 794,
y: -3.396e1,
}, {
x: 1000,
y: -3.596e1,
},]
}]
};
$.each(scatterChartData.datasets, function(i, dataset) {
dataset.borderColor = randomColor(0.4);
dataset.backgroundColor = randomColor(0.1);
dataset.pointBorderColor = randomColor(0.7);
dataset.pointBackgroundColor = randomColor(0.5);
dataset.pointBorderWidth = 1;
});
window.onload = function() {
var ctx = document.getElementById("canvas").getContext("2d");
window.myScatter = Chart.Scatter(ctx, {
data: scatterChartData,
options: {
scales: {
xAxes: [{
type: 'logarithmic',
position: 'bottom',
labels: {
userCallback: function(tick) {
var remain = tick / (Math.pow(10, Math.floor(Chart.helpers.log10(tick))));
if (remain === 1 || remain === 2 || remain === 5) {
return tick.toString() + "Hz";
}
return '';
}
}
}],
yAxes: [{
type: 'linear',
labels: {
userCallback: function(tick) {
return tick.toString() + "dB";
}
}
}]
}
}
});
};
</script>
</body>
</html>

View File

@ -330,10 +330,12 @@
var eventPosition = helpers.getRelativePosition(e);
var elementsArray = [];
for (var datasetIndex = 0; datasetIndex < this.chart.data.datasets.length; datasetIndex++) {
for (elementIndex = 0; elementIndex < this.chart.data.datasets[datasetIndex].metaData.length; elementIndex++) {
if (this.chart.data.datasets[datasetIndex].metaData[elementIndex].inLabelRange(eventPosition.x, eventPosition.y)) {
helpers.each(this.chart.data.datasets, datasetIterator);
for (var datasetIndex = 0; datasetIndex < this.data.datasets.length; datasetIndex++) {
for (var elementIndex = 0; elementIndex < this.data.datasets[datasetIndex].metaData.length; elementIndex++) {
if (this.data.datasets[datasetIndex].metaData[elementIndex].inLabelRange(eventPosition.x, eventPosition.y)) {
helpers.each(this.data.datasets[datasetIndex].metaData, function(element, index) {
elementsArray.push(element);
}, this);
}
}
}
@ -420,12 +422,11 @@
this.data.datasets[this.lastActive[0]._datasetIndex].controller.removeHoverStyle(this.lastActive[0], this.lastActive[0]._datasetIndex, this.lastActive[0]._index);
break;
case 'label':
case 'dataset':
for (var i = 0; i < this.lastActive.length; i++) {
this.data.datasets[this.lastActive[i]._datasetIndex].controller.removeHoverStyle(this.lastActive[i], this.lastActive[i]._datasetIndex, this.lastActive[i]._index);
}
break;
case 'dataset':
break;
default:
// Don't change anything
}
@ -438,12 +439,11 @@
this.data.datasets[this.active[0]._datasetIndex].controller.setHoverStyle(this.active[0]);
break;
case 'label':
case 'dataset':
for (var i = 0; i < this.active.length; i++) {
this.data.datasets[this.active[i]._datasetIndex].controller.setHoverStyle(this.active[i]);
}
break;
case 'dataset':
break;
default:
// Don't change anything
}

View File

@ -387,8 +387,7 @@
} else {
// Generate a reusable function that will serve as a template
// generator (and which will be cached).
fn = new Function("obj",
"var p=[],print=function(){p.push.apply(p,arguments);};" +
var functionCode = "var p=[],print=function(){p.push.apply(p,arguments);};" +
// Introduce the data as local variables using with(){}
"with(obj){p.push('" +
@ -402,8 +401,8 @@
.split("\t").join("');")
.split("%>").join("p.push('")
.split("\r").join("\\'") +
"');}return p.join('');"
);
"');}return p.join('');";
fn = new Function("obj", functionCode);
// Cache the result
templateStringCache[str] = fn;

View File

@ -164,7 +164,11 @@
x: medianPosition.x,
y: medianPosition.y,
labels: labels,
title: this._data.labels && this._data.labels.length ? this._data.labels[this._active[0]._index] : '',
title: (function() {
return this._data.timeLabels ? this._data.timeLabels[this._active[0]._index] :
(this._data.labels && this._data.labels.length) ? this._data.labels[this._active[0]._index] :
'';
}).call(this),
legendColors: colors,
legendBackgroundColor: this._options.tooltips.multiKeyBackground,
});

View File

@ -0,0 +1,589 @@
(function() {
"use strict";
var root = this,
Chart = root.Chart,
helpers = Chart.helpers;
var defaultConfig = {
display: true,
position: "left",
// grid line settings
gridLines: {
show: true,
color: "rgba(0, 0, 0, 0.1)",
lineWidth: 1,
drawOnChartArea: true,
drawTicks: true, // draw ticks extending towards the label
zeroLineWidth: 1,
zeroLineColor: "rgba(0,0,0,0.25)",
},
// scale numbers
reverse: false,
override: null,
// label settings
labels: {
show: true,
mirror: false,
padding: 10,
template: "<%var remain = value / (Math.pow(10, Math.floor(Chart.helpers.log10(value))));if (remain === 1 || remain === 2 || remain === 5) {%><%=value.toExponential()%><%} else {%><%= null %><%}%>",
fontSize: 12,
fontStyle: "normal",
fontColor: "#666",
fontFamily: "Helvetica Neue"
}
};
var LogarithmicScale = Chart.Element.extend({
isHorizontal: function() {
return this.options.position == "top" || this.options.position == "bottom";
},
generateTicks: function(width, height) {
// We need to decide how many ticks we are going to have. Each tick draws a grid line.
// There are two possibilities. The first is that the user has manually overridden the scale
// calculations in which case the job is easy. The other case is that we have to do it ourselves
//
// We assume at this point that the scale object has been updated with the following values
// by the chart.
// min: this is the minimum value of the scale
// max: this is the maximum value of the scale
// options: contains the options for the scale. This is referenced from the user settings
// rather than being cloned. This ensures that updates always propogate to a redraw
// Reset the ticks array. Later on, we will draw a grid line at these positions
// The array simply contains the numerical value of the spots where ticks will be
this.ticks = [];
if (this.options.override) {
// The user has specified the manual override. We use <= instead of < so that
// we get the final line
for (var i = 0; i <= this.options.override.steps; ++i) {
var value = this.options.override.start + (i * this.options.override.stepWidth);
this.ticks.push(value);
}
} else {
// Figure out what the max number of ticks we can support it is based on the size of
// the axis area. For now, we say that the minimum tick spacing in pixels must be 50
// We also limit the maximum number of ticks to 11 which gives a nice 10 squares on
// the graph
var minExponent = Math.floor(helpers.log10(this.min));
var maxExponent = Math.ceil(helpers.log10(this.max));
for (var exponent = minExponent; exponent < maxExponent; ++exponent) {
for (var i = 1; i < 10; ++i) {
this.ticks.push(i * Math.pow(10, exponent));
}
}
this.ticks.push(1.0 * Math.pow(10, maxExponent));
}
if (this.options.position == "left" || this.options.position == "right") {
// We are in a vertical orientation. The top value is the highest. So reverse the array
this.ticks.reverse();
}
// At this point, we need to update our max and min given the tick values since we have expanded the
// range of the scale
this.max = helpers.max(this.ticks);
this.min = helpers.min(this.ticks);
if (this.options.reverse) {
this.ticks.reverse();
this.start = this.max;
this.end = this.min;
} else {
this.start = this.min;
this.end = this.max;
}
},
buildLabels: function() {
// We assume that this has been run after ticks have been generated. We try to figure out
// a label for each tick.
this.labels = [];
helpers.each(this.ticks, function(tick, index, ticks) {
var label;
if (this.options.labels.userCallback) {
// If the user provided a callback for label generation, use that as first priority
label = this.options.labels.userCallback(tick, index, ticks);
} else if (this.options.labels.template) {
// else fall back to the template string
label = helpers.template(this.options.labels.template, {
value: tick
});
}
this.labels.push(label); // empty string will not render so we're good
}, this);
},
// Get the correct value. If the value type is object get the x or y based on whether we are horizontal or not
getRightValue: function(rawValue) {
return typeof rawValue === "object" ? (this.isHorizontal() ? rawValue.x : rawValue.y) : rawValue;
},
getPixelForValue: function(value) {
// This must be called after fit has been run so that
// this.left, this.top, this.right, and this.bottom have been defined
var pixel;
var range = helpers.log10(this.end) - helpers.log10(this.start);
if (this.isHorizontal()) {
if (value === 0) {
pixel = this.left + this.paddingLeft;
} else {
var innerWidth = this.width - (this.paddingLeft + this.paddingRight);
pixel = this.left + (innerWidth / range * (helpers.log10(value) - helpers.log10(this.start)));
pixel += this.paddingLeft;
}
} else {
// Bottom - top since pixels increase downard on a screen
if (value === 0) {
pixel = this.top + this.paddingTop;
} else {
var innerHeight = this.height - (this.paddingTop + this.paddingBottom);
pixel = (this.bottom - this.paddingBottom) - (innerHeight / range * (helpers.log10(value) - helpers.log10(this.start)));
}
}
return pixel;
},
// Functions needed for line charts
calculateRange: function() {
this.min = null;
this.max = null;
var values = [];
if (this.options.stacked) {
helpers.each(this.data.datasets, function(dataset) {
if (this.isHorizontal() ? dataset.xAxisID === this.id : dataset.yAxisID === this.id) {
helpers.each(dataset.data, function(rawValue, index) {
var value = this.getRightValue(rawValue);
values[index] = values[index] || 0;
if (this.options.relativePoints) {
values[index] = 100;
} else {
// Don't need to split positive and negative since the log scale can't handle a 0 crossing
values[index] += value;
}
}, this);
}
}, this);
this.min = helpers.min(values);
this.max = helpers.max(values);
} else {
helpers.each(this.data.datasets, function(dataset) {
if (this.isHorizontal() ? dataset.xAxisID === this.id : dataset.yAxisID === this.id) {
helpers.each(dataset.data, function(rawValue, index) {
var value = this.getRightValue(rawValue);
if (this.min === null) {
this.min = value;
} else if (value < this.min) {
this.min = value;
}
if (this.max === null) {
this.max = value;
} else if (value > this.max) {
this.max = value;
}
}, this);
}
}, this);
}
if (this.min === this.max) {
if (this.min !== 0 && this.min !== null) {
this.min = Math.pow(10, Math.floor(helpers.log10(this.min)) - 1);
this.max = Math.pow(10, Math.floor(helpers.log10(this.max)) + 1);
} else {
this.min = 1;
this.max = 10;
}
}
},
getPointPixelForValue: function(rawValue, index, datasetIndex) {
var value = this.getRightValue(rawValue);
if (this.options.stacked) {
var offsetPos = 0;
var offsetNeg = 0;
for (var i = this.data.datasets.length - 1; i > datasetIndex; --i) {
if (this.data.datasets[i].data[index] < 0) {
offsetNeg += this.data.datasets[i].data[index];
} else {
offsetPos += this.data.datasets[i].data[index];
}
}
if (value < 0) {
return this.getPixelForValue(offsetNeg + value);
} else {
return this.getPixelForValue(offsetPos + value);
}
} else {
return this.getPixelForValue(value);
}
},
// Functions needed for bar charts
calculateBarBase: function(datasetIndex, index) {
var base = 0;
if (this.options.stacked) {
var value = this.data.datasets[datasetIndex].data[index];
for (var j = 0; j < datasetIndex; j++) {
if (this.data.datasets[j].yAxisID === this.id) {
base += this.data.datasets[j].data[index];
}
}
return this.getPixelForValue(base);
}
base = this.getPixelForValue(this.min);
if (this.min < 0 && this.max < 0) {
// All values are negative. Use the top as the base
base = this.getPixelForValue(this.max);
}
return base;
},
calculateBarY: function(datasetIndex, index) {
var value = this.data.datasets[datasetIndex].data[index];
if (this.options.stacked) {
var sumPos = 0,
sumNeg = 0;
for (var i = 0; i < datasetIndex; i++) {
if (this.data.datasets[i].data[index] < 0) {
sumNeg += this.data.datasets[i].data[index] || 0;
} else {
sumPos += this.data.datasets[i].data[index] || 0;
}
}
if (value < 0) {
return this.getPixelForValue(sumNeg + value);
} else {
return this.getPixelForValue(sumPos + value);
}
return this.getPixelForValue(value);
}
var offset = 0;
for (var j = datasetIndex; j < this.data.datasets.length; j++) {
if (j === datasetIndex && value) {
offset += value;
} else {
offset = offset + value;
}
}
return this.getPixelForValue(value);
},
// Fit this axis to the given size
// @param {number} maxWidth : the max width the axis can be
// @param {number} maxHeight: the max height the axis can be
// @return {object} minSize : the minimum size needed to draw the axis
fit: function(maxWidth, maxHeight, margins) {
this.calculateRange();
this.generateTicks(maxWidth, maxHeight);
this.buildLabels();
var minSize = {
width: 0,
height: 0,
};
// In a horizontal axis, we need some room for the scale to be drawn
//
// -----------------------------------------------------
// | | | | |
//
// In a vertical axis, we need some room for the scale to be drawn.
// The actual grid lines will be drawn on the chart area, however, we need to show
// ticks where the axis actually is.
// We will allocate 25px for this width
// |
// -|
// |
// |
// -|
// |
// |
// -|
// Width
if (this.isHorizontal()) {
minSize.width = maxWidth; // fill all the width
} else {
minSize.width = this.options.gridLines.show && this.options.display ? 10 : 0;
}
// height
if (this.isHorizontal()) {
minSize.height = this.options.gridLines.show && this.options.display ? 10 : 0;
} else {
minSize.height = maxHeight; // fill all the height
}
this.paddingLeft = 0;
this.paddingRight = 0;
this.paddingTop = 0;
this.paddingBottom = 0;
if (this.options.labels.show && this.options.display) {
// Don't bother fitting the labels if we are not showing them
var labelFont = helpers.fontString(this.options.labels.fontSize,
this.options.labels.fontStyle, this.options.labels.fontFamily);
if (this.isHorizontal()) {
// A horizontal axis is more constrained by the height.
var maxLabelHeight = maxHeight - minSize.height;
var labelHeight = 1.5 * this.options.labels.fontSize;
minSize.height = Math.min(maxHeight, minSize.height + labelHeight);
var labelFont = helpers.fontString(this.options.labels.fontSize, this.options.labels.fontStyle, this.options.labels.fontFamily);
this.ctx.font = labelFont;
var firstLabelWidth = this.ctx.measureText(this.labels[0]).width;
var lastLabelWidth = this.ctx.measureText(this.labels[this.labels.length - 1]).width;
// Ensure that our labels are always inside the canvas
this.paddingLeft = firstLabelWidth / 2;
this.paddingRight = lastLabelWidth / 2;
} else {
// A vertical axis is more constrained by the width. Labels are the dominant factor
// here, so get that length first
var maxLabelWidth = maxWidth - minSize.width;
var largestTextWidth = helpers.longestText(this.ctx, labelFont, this.labels);
if (largestTextWidth < maxLabelWidth) {
// We don't need all the room
minSize.width += largestTextWidth;
minSize.width += 3; // extra padding
} else {
// Expand to max size
minSize.width = maxWidth;
}
this.paddingTop = this.options.labels.fontSize / 2;
this.paddingBottom = this.options.labels.fontSize / 2;
}
}
if (margins) {
this.paddingLeft -= margins.left;
this.paddingTop -= margins.top;
this.paddingRight -= margins.right;
this.paddingBottom -= margins.bottom;
this.paddingLeft = Math.max(this.paddingLeft, 0);
this.paddingTop = Math.max(this.paddingTop, 0);
this.paddingRight = Math.max(this.paddingRight, 0);
this.paddingBottom = Math.max(this.paddingBottom, 0);
}
this.width = minSize.width;
this.height = minSize.height;
return minSize;
},
// Actualy draw the scale on the canvas
// @param {rectangle} chartArea : the area of the chart to draw full grid lines on
draw: function(chartArea) {
if (this.options.display) {
var setContextLineSettings;
var hasZero;
// Make sure we draw text in the correct color
this.ctx.fillStyle = this.options.labels.fontColor;
if (this.isHorizontal()) {
if (this.options.gridLines.show) {
// Draw the horizontal line
setContextLineSettings = true;
hasZero = helpers.findNextWhere(this.ticks, function(tick) {
return tick === 0;
}) !== undefined;
var yTickStart = this.options.position == "bottom" ? this.top : this.bottom - 5;
var yTickEnd = this.options.position == "bottom" ? this.top + 5 : this.bottom;
helpers.each(this.ticks, function(tick, index) {
// Grid lines are vertical
var xValue = this.getPixelForValue(tick);
if (this.labels[index] === null) {
// If the user specifically hid the label by returning null from the label function, do so
return;
}
if (tick === 0 || (!hasZero && index === 0)) {
// Draw the 0 point specially or the left if there is no 0
this.ctx.lineWidth = this.options.gridLines.zeroLineWidth;
this.ctx.strokeStyle = this.options.gridLines.zeroLineColor;
setContextLineSettings = true; // reset next time
} else if (setContextLineSettings) {
this.ctx.lineWidth = this.options.gridLines.lineWidth;
this.ctx.strokeStyle = this.options.gridLines.color;
setContextLineSettings = false;
}
xValue += helpers.aliasPixel(this.ctx.lineWidth);
// Draw the label area
this.ctx.beginPath();
if (this.options.gridLines.drawTicks) {
this.ctx.moveTo(xValue, yTickStart);
this.ctx.lineTo(xValue, yTickEnd);
}
// Draw the chart area
if (this.options.gridLines.drawOnChartArea) {
this.ctx.moveTo(xValue, chartArea.top);
this.ctx.lineTo(xValue, chartArea.bottom);
}
// Need to stroke in the loop because we are potentially changing line widths & colours
this.ctx.stroke();
}, this);
}
if (this.options.labels.show) {
// Draw the labels
var labelStartY;
if (this.options.position == "top") {
labelStartY = this.bottom - 10;
this.ctx.textBaseline = "bottom";
} else {
// bottom side
labelStartY = this.top + 10;
this.ctx.textBaseline = "top";
}
this.ctx.textAlign = "center";
this.ctx.font = helpers.fontString(this.options.labels.fontSize, this.options.labels.fontStyle, this.options.labels.fontFamily);
helpers.each(this.labels, function(label, index) {
var xValue = this.getPixelForValue(this.ticks[index]);
if (label) {
this.ctx.fillText(label, xValue, labelStartY);
}
}, this);
}
} else {
// Vertical
if (this.options.gridLines.show) {
// Draw the vertical line
setContextLineSettings = true;
hasZero = helpers.findNextWhere(this.ticks, function(tick) {
return tick === 0;
}) !== undefined;
var xTickStart = this.options.position == "right" ? this.left : this.right - 5;
var xTickEnd = this.options.position == "right" ? this.left + 5 : this.right;
helpers.each(this.ticks, function(tick, index) {
// Grid lines are horizontal
var yValue = this.getPixelForValue(tick);
if (tick === 0 || (!hasZero && index === 0)) {
// Draw the 0 point specially or the bottom if there is no 0
this.ctx.lineWidth = this.options.gridLines.zeroLineWidth;
this.ctx.strokeStyle = this.options.gridLines.zeroLineColor;
setContextLineSettings = true; // reset next time
} else if (setContextLineSettings) {
this.ctx.lineWidth = this.options.gridLines.lineWidth;
this.ctx.strokeStyle = this.options.gridLines.color;
setContextLineSettings = false; // use boolean to indicate that we only want to do this once
}
yValue += helpers.aliasPixel(this.ctx.lineWidth);
// Draw the label area
this.ctx.beginPath();
if (this.options.gridLines.drawTicks) {
this.ctx.moveTo(xTickStart, yValue);
this.ctx.lineTo(xTickEnd, yValue);
}
// Draw the chart area
if (this.options.gridLines.drawOnChartArea) {
this.ctx.moveTo(chartArea.left, yValue);
this.ctx.lineTo(chartArea.right, yValue);
}
this.ctx.stroke();
}, this);
}
if (this.options.labels.show) {
// Draw the labels
var labelStartX;
if (this.options.position == "left") {
if (this.options.labels.mirror) {
labelStartX = this.right + this.options.labels.padding;
this.ctx.textAlign = "left";
} else {
labelStartX = this.right - this.options.labels.padding;
this.ctx.textAlign = "right";
}
} else {
// right side
if (this.options.labels.mirror) {
labelStartX = this.left - this.options.labels.padding;
this.ctx.textAlign = "right";
} else {
labelStartX = this.left + this.options.labels.padding;
this.ctx.textAlign = "left";
}
}
this.ctx.textBaseline = "middle";
this.ctx.font = helpers.fontString(this.options.labels.fontSize, this.options.labels.fontStyle, this.options.labels.fontFamily);
helpers.each(this.labels, function(label, index) {
var yValue = this.getPixelForValue(this.ticks[index]);
this.ctx.fillText(label, labelStartX, yValue);
}, this);
}
}
}
}
});
Chart.scaleService.registerScaleType("logarithmic", LogarithmicScale, defaultConfig);
}).call(this);

437
src/scales/scale.time.js Normal file
View File

@ -0,0 +1,437 @@
(function() {
"use strict";
var root = this,
Chart = root.Chart,
helpers = Chart.helpers;
var time = {
units: [
'millisecond',
'second',
'minute',
'hour',
'day',
'week',
'month',
'quarter',
'year',
],
unit: {
'millisecond': {
display: 'SSS [ms]', // 002 ms
maxStep: 1000,
},
'second': {
display: 'h:mm:ss a', // 11:20:01 AM
maxStep: 60,
},
'minute': {
display: 'h:mm:ss a', // 11:20:01 AM
maxStep: 60,
},
'hour': {
display: 'MMM D, hA', // Sept 4, 5PM
maxStep: 24,
},
'day': {
display: 'll', // Sep 4 2015
maxStep: 7,
},
'week': {
display: 'll', // Week 46, or maybe "[W]WW - YYYY" ?
maxStep: 4.3333,
},
'month': {
display: 'MMM YYYY', // Sept 2015
maxStep: 12,
},
'quarter': {
display: '[Q]Q - YYYY', // Q3
maxStep: 4,
},
'year': {
display: 'YYYY', // 2015
maxStep: false,
},
}
};
var defaultConfig = {
display: true,
position: "bottom",
// grid line settings
gridLines: {
show: true,
color: "rgba(0, 0, 0, 0.1)",
lineWidth: 1,
drawOnChartArea: true,
drawTicks: true, // draw ticks extending towards the label
},
tick: {
format: false, // false == date objects or use pattern string from http://momentjs.com/docs/#/parsing/string-format/
unit: false, // false == automatic or override with week, month, year, etc.
round: false, // none, or override with week, month, year, etc.
displayFormat: false, // defaults to unit's corresponding unitFormat below or override using pattern string from http://momentjs.com/docs/#/displaying/format/
},
// scale numbers
reverse: false,
override: null,
// label settings
labels: {
show: true,
mirror: false,
padding: 10,
template: "<%=value.toLocaleString()%>",
fontSize: 12,
fontStyle: "normal",
fontColor: "#666",
fontFamily: "Helvetica Neue",
maxRotation: 45,
}
};
var TimeScale = Chart.Element.extend({
isHorizontal: function() {
return this.options.position == "top" || this.options.position == "bottom";
},
parseTime: function(label) {
// Date objects
if (typeof label.getMonth === 'function' || typeof label == 'number') {
return moment(label);
}
// Moment support
if (label.isValid && label.isValid()) {
return label;
}
// Custom parsing (return an instance of moment)
if (typeof this.options.tick.format !== 'string' && this.options.tick.format.call) {
return this.options.tick.format(label);
}
// Moment format parsing
return moment(label, this.options.tick.format);
},
generateTicks: function(index) {
this.ticks = [];
this.labelMoments = [];
// Parse each label into a moment
this.data.labels.forEach(function(label, index) {
var labelMoment = this.parseTime(label);
if (this.options.tick.round) {
labelMoment.startOf(this.options.tick.round);
}
this.labelMoments.push(labelMoment);
}, this);
// Find the first and last moments, and range
this.firstTick = moment.min.call(this, this.labelMoments).clone();
this.lastTick = moment.max.call(this, this.labelMoments).clone();
// Set unit override if applicable
if (this.options.tick.unit) {
this.tickUnit = this.options.tick.unit || 'day';
this.displayFormat = time.unit.day.display;
this.tickRange = Math.ceil(this.lastTick.diff(this.firstTick, this.tickUnit, true));
} else {
// Determine the smallest needed unit of the time
var innerWidth = this.width - (this.paddingLeft + this.paddingRight);
var labelCapacity = innerWidth / this.options.labels.fontSize + 4;
var buffer = this.options.tick.round ? 0 : 2;
this.tickRange = Math.ceil(this.lastTick.diff(this.firstTick, true) + buffer);
var done;
helpers.each(time.units, function(format) {
if (this.tickRange <= labelCapacity) {
return;
}
this.tickUnit = format;
this.tickRange = Math.ceil(this.lastTick.diff(this.firstTick, this.tickUnit) + buffer);
this.displayFormat = time.unit[format].display;
}, this);
}
this.firstTick.startOf(this.tickUnit);
this.lastTick.endOf(this.tickUnit);
// Tick displayFormat override
if (this.options.tick.displayFormat) {
this.displayFormat = this.options.tick.displayFormat;
}
// For every unit in between the first and last moment, create a moment and add it to the labels tick
var i = 0;
if (this.options.labels.userCallback) {
for (; i <= this.tickRange; i++) {
this.ticks.push(
this.options.labels.userCallback(this.firstTick.clone()
.add(i, this.tickUnit)
.format(this.options.tick.displayFormat ? this.options.tick.displayFormat : time.unit[this.tickUnit].display)
)
);
}
} else {
for (; i <= this.tickRange; i++) {
this.ticks.push(this.firstTick.clone()
.add(i, this.tickUnit)
.format(this.options.tick.displayFormat ? this.options.tick.displayFormat : time.unit[this.tickUnit].display)
);
}
}
},
getPixelForValue: function(value, decimal, datasetIndex, includeOffset) {
// This must be called after fit has been run so that
// this.left, this.top, this.right, and this.bottom have been defined
if (this.isHorizontal()) {
var innerWidth = this.width - (this.paddingLeft + this.paddingRight);
var valueWidth = innerWidth / Math.max((this.ticks.length - ((this.options.gridLines.offsetGridLines) ? 0 : 1)), 1);
var valueOffset = (innerWidth * decimal) + this.paddingLeft;
if (this.options.gridLines.offsetGridLines && includeOffset) {
valueOffset += (valueWidth / 2);
}
return this.left + Math.round(valueOffset);
} else {
return this.top + (decimal * (this.height / this.ticks.length));
}
},
getPointPixelForValue: function(value, index, datasetIndex) {
var offset = this.labelMoments[index].diff(this.firstTick, this.tickUnit, true);
return this.getPixelForValue(value, offset / this.tickRange, datasetIndex);
},
// Functions needed for bar charts
calculateBaseWidth: function() {
return (this.getPixelForValue(null, this.ticks.length / 100, 0, true) - this.getPixelForValue(null, 0, 0, true)) - (2 * this.options.categorySpacing);
},
calculateBarWidth: function(barDatasetCount) {
//The padding between datasets is to the right of each bar, providing that there are more than 1 dataset
var baseWidth = this.calculateBaseWidth() - ((barDatasetCount - 1) * this.options.spacing);
if (this.options.stacked) {
return baseWidth;
}
return (baseWidth / barDatasetCount);
},
calculateBarX: function(barDatasetCount, datasetIndex, elementIndex) {
var xWidth = this.calculateBaseWidth(),
xAbsolute = this.getPixelForValue(null, elementIndex, datasetIndex, true) - (xWidth / 2),
barWidth = this.calculateBarWidth(barDatasetCount);
if (this.options.stacked) {
return xAbsolute + barWidth / 2;
}
return xAbsolute + (barWidth * datasetIndex) + (datasetIndex * this.options.spacing) + barWidth / 2;
},
calculateTickRotation: function(maxHeight, margins) {
//Get the width of each grid by calculating the difference
//between x offsets between 0 and 1.
var labelFont = helpers.fontString(this.options.labels.fontSize, this.options.labels.fontStyle, this.options.labels.fontFamily);
this.ctx.font = labelFont;
var firstWidth = this.ctx.measureText(this.ticks[0]).width;
var lastWidth = this.ctx.measureText(this.ticks[this.ticks.length - 1]).width;
var firstRotated;
var lastRotated;
this.paddingRight = lastWidth / 2 + 3;
this.paddingLeft = firstWidth / 2 + 3;
this.labelRotation = 0;
if (this.options.display) {
var originalLabelWidth = helpers.longestText(this.ctx, labelFont, this.ticks);
var cosRotation;
var sinRotation;
var firstRotatedWidth;
this.labelWidth = originalLabelWidth;
//Allow 3 pixels x2 padding either side for label readability
// only the index matters for a dataset scale, but we want a consistent interface between scales
var datasetWidth = Math.floor(this.getPixelForValue(null, 1 / this.ticks.length) - this.getPixelForValue(null, 0)) - 6;
//Max label rotation can be set or default to 90 - also act as a loop counter
while (this.labelWidth > datasetWidth && this.labelRotation <= this.options.labels.maxRotation) {
cosRotation = Math.cos(helpers.toRadians(this.labelRotation));
sinRotation = Math.sin(helpers.toRadians(this.labelRotation));
firstRotated = cosRotation * firstWidth;
lastRotated = cosRotation * lastWidth;
// We're right aligning the text now.
if (firstRotated + this.options.labels.fontSize / 2 > this.yLabelWidth) {
this.paddingLeft = firstRotated + this.options.labels.fontSize / 2;
}
this.paddingRight = this.options.labels.fontSize / 2;
if (sinRotation * originalLabelWidth > maxHeight) {
// go back one step
this.labelRotation--;
break;
}
this.labelRotation++;
this.labelWidth = cosRotation * originalLabelWidth;
}
} else {
this.labelWidth = 0;
this.paddingRight = 0;
this.paddingLeft = 0;
}
if (margins) {
this.paddingLeft -= margins.left;
this.paddingRight -= margins.right;
this.paddingLeft = Math.max(this.paddingLeft, 0);
this.paddingRight = Math.max(this.paddingRight, 0);
}
},
// Fit this axis to the given size
// @param {number} maxWidth : the max width the axis can be
// @param {number} maxHeight: the max height the axis can be
// @return {object} minSize : the minimum size needed to draw the axis
fit: function(maxWidth, maxHeight, margins) {
// Set the unconstrained dimension before label rotation
if (this.isHorizontal()) {
this.width = maxWidth;
} else {
this.height = maxHeight;
}
this.generateTicks();
this.calculateTickRotation(maxHeight, margins);
var minSize = {
width: 0,
height: 0,
};
var labelFont = helpers.fontString(this.options.labels.fontSize, this.options.labels.fontStyle, this.options.labels.fontFamily);
var longestLabelWidth = helpers.longestText(this.ctx, labelFont, this.ticks);
// Width
if (this.isHorizontal()) {
minSize.width = maxWidth;
} else if (this.options.display) {
var labelWidth = this.options.labels.show ? longestLabelWidth + 6 : 0;
minSize.width = Math.min(labelWidth, maxWidth);
}
// Height
if (this.isHorizontal() && this.options.display) {
var labelHeight = (Math.sin(helpers.toRadians(this.labelRotation)) * longestLabelWidth) + 1.5 * this.options.labels.fontSize;
minSize.height = Math.min(this.options.labels.show ? labelHeight : 0, maxHeight);
} else if (this.options.display) {
minSize.height = maxHeight;
}
this.width = minSize.width;
this.height = minSize.height;
return minSize;
},
// Actualy draw the scale on the canvas
// @param {rectangle} chartArea : the area of the chart to draw full grid lines on
draw: function(chartArea) {
if (this.options.display) {
var setContextLineSettings;
// Make sure we draw text in the correct color
this.ctx.fillStyle = this.options.labels.fontColor;
if (this.isHorizontal()) {
setContextLineSettings = true;
var yTickStart = this.options.position == "bottom" ? this.top : this.bottom - 10;
var yTickEnd = this.options.position == "bottom" ? this.top + 10 : this.bottom;
var isRotated = this.labelRotation !== 0;
var skipRatio = false;
if ((this.options.labels.fontSize + 4) * this.ticks.length > (this.width - (this.paddingLeft + this.paddingRight))) {
skipRatio = 1 + Math.floor(((this.options.labels.fontSize + 4) * this.ticks.length) / (this.width - (this.paddingLeft + this.paddingRight)));
}
helpers.each(this.ticks, function(tick, index) {
// Blank ticks
if ((skipRatio > 1 && index % skipRatio > 0) || (tick === undefined || tick === null)) {
return;
}
var xLineValue = this.getPixelForValue(null, (1 / (this.ticks.length - 1)) * index, null, false); // xvalues for grid lines
var xLabelValue = this.getPixelForValue(null, (1 / (this.ticks.length - 1)) * index, null, true); // x values for ticks (need to consider offsetLabel option)
if (this.options.gridLines.show) {
if (index === 0) {
// Draw the first index specially
this.ctx.lineWidth = this.options.gridLines.zeroLineWidth;
this.ctx.strokeStyle = this.options.gridLines.zeroLineColor;
setContextLineSettings = true; // reset next time
} else if (setContextLineSettings) {
this.ctx.lineWidth = this.options.gridLines.lineWidth;
this.ctx.strokeStyle = this.options.gridLines.color;
setContextLineSettings = false;
}
xLineValue += helpers.aliasPixel(this.ctx.lineWidth);
// Draw the tick area
this.ctx.beginPath();
if (this.options.gridLines.drawTicks) {
this.ctx.moveTo(xLineValue, yTickStart);
this.ctx.lineTo(xLineValue, yTickEnd);
}
// Draw the chart area
if (this.options.gridLines.drawOnChartArea) {
this.ctx.moveTo(xLineValue, chartArea.top);
this.ctx.lineTo(xLineValue, chartArea.bottom);
}
// Need to stroke in the loop because we are potentially changing line widths & colours
this.ctx.stroke();
}
if (this.options.labels.show) {
this.ctx.save();
this.ctx.translate(xLabelValue, (isRotated) ? this.top + 12 : this.top + 8);
this.ctx.rotate(helpers.toRadians(this.labelRotation) * -1);
this.ctx.font = this.font;
this.ctx.textAlign = (isRotated) ? "right" : "center";
this.ctx.textBaseline = (isRotated) ? "middle" : "top";
this.ctx.fillText(tick, 0, 0);
this.ctx.restore();
}
}, this);
} else {
// Vertical
if (this.options.gridLines.show) {}
if (this.options.labels.show) {
// Draw the labels
}
}
}
}
});
Chart.scaleService.registerScaleType("time", TimeScale, defaultConfig);
}).call(this);

File diff suppressed because it is too large Load Diff