No hardcoded chart type methods

This commit is contained in:
Tanner Linsley 2015-06-14 19:15:10 -06:00
parent 4047440acc
commit d73dae09e7
12 changed files with 377 additions and 400 deletions

View File

@ -33,7 +33,6 @@ gulp.task('build', function() {
'./src/scales/**',
'./src/elements/**',
'./src/charts/**',
'./src/**',
'./node_modules/color/dist/color.min.js'
],
isCustom = !!(util.env.types),

View File

@ -39,7 +39,7 @@
};
window.onload = function() {
var ctx = document.getElementById("canvas").getContext("2d");
window.myBar = new Chart(ctx).Bar({
window.myBar = new Chart(ctx, {
data: barChartData,
options: {
responsive: true,

View File

@ -25,7 +25,6 @@
type: "linear",
}],
},
};

View File

@ -5,191 +5,5 @@
Chart = root.Chart,
helpers = Chart.helpers;
Chart.RectangularCanvasController = function(chart, elementController) {
this.chartInstance = chart;
this.elementController = elementController;
};
Chart.RectangularCanvasController.prototype.initialize = function() {
this.bindEvents();
this.buildScales();
// Need to fit scales before we reset elements.
Chart.scaleService.fitScalesForChart(this.chartInstance, this.chartInstance.chart.width, this.chartInstance.chart.height);
this.elementController.resetElements();
this.initToolTip();
this.chartInstance.update();
};
Chart.RectangularCanvasController.prototype.bindEvents = function() {
helpers.bindEvents(this.chartInstance, this.chartInstance.options.events, function(evt) {
// this will be the chart instance
this.canvasController.eventHandler(evt);
});
};
Chart.RectangularCanvasController.prototype.eventHandler = function(e) {
this.lastActive = this.lastActive || [];
// Find Active Elements
if (e.type == 'mouseout') {
this.active = [];
} else {
this.active = function() {
switch (this.chartInstance.options.hover.mode) {
case 'single':
return this.elementController.getElementAtEvent(e);
case 'label':
return this.elementController.getElementsAtEvent(e);
case 'dataset':
return this.elementController.getDatasetAtEvent(e);
default:
return e;
}
}.call(this);
}
// On Hover hook
if (this.chartInstance.options.hover.onHover) {
this.chartInstance.options.hover.onHover.call(this.chartInstance, this.active);
}
if (e.type == 'mouseup' || e.type == 'click') {
if (this.chartInstance.options.onClick) {
this.chartInstance.options.onClick.call(this, e, this.active);
}
}
var dataset;
var index;
// Remove styling for last active (even if it may still be active)
if (this.lastActive.length) {
switch (this.chartInstance.options.hover.mode) {
case 'single':
this.elementController.resetElementAppearance(this.lastActive[0], this.lastActive[0]._datasetIndex, this.lastActive[0]._index);
break;
case 'label':
for (var i = 0; i < this.lastActive.length; i++) {
this.elementController.resetElementAppearance(this.lastActive[i], this.lastActive[i]._datasetIndex, this.lastActive[i]._index);
}
break;
case 'dataset':
break;
default:
// Don't change anything
}
}
// Built in hover styling
if (this.active.length && this.chartInstance.options.hover.mode) {
switch (this.chartInstance.options.hover.mode) {
case 'single':
this.elementController.setElementHoverStyle(this.active[0]);
break;
case 'label':
for (var i = 0; i < this.active.length; i++) {
this.elementController.setElementHoverStyle(this.active[i]);
}
break;
case 'dataset':
break;
default:
// Don't change anything
}
}
// Built in Tooltips
if (this.chartInstance.options.tooltips.enabled) {
// The usual updates
this.chartInstance.tooltip.initialize();
// Active
if (this.active.length) {
this.chartInstance.tooltip._model.opacity = 1;
helpers.extend(this.chartInstance.tooltip, {
_active: this.active,
});
this.chartInstance.tooltip.update();
} else {
// Inactive
this.chartInstance.tooltip._model.opacity = 0;
}
}
// Hover animations
this.chartInstance.tooltip.pivot();
if (!this.chartInstance.animating) {
var changed;
helpers.each(this.active, function(element, index) {
if (element !== this.lastActive[index]) {
changed = true;
}
}, this);
// If entering, leaving, or changing elements, animate the change via pivot
if ((!this.lastActive.length && this.active.length) ||
(this.lastActive.length && !this.active.length) ||
(this.lastActive.length && this.active.length && changed)) {
this.chartInstance.stop();
this.chartInstance.render(this.chartInstance.options.hover.animationDuration);
}
}
// Remember Last Active
this.lastActive = this.active;
return this;
};
Chart.RectangularCanvasController.prototype.initToolTip = function() {
this.chartInstance.tooltip = new Chart.Tooltip({
_chart: this.chartInstance.chart,
_data: this.chartInstance.data,
_options: this.chartInstance.options,
}, this);
};
Chart.RectangularCanvasController.prototype.buildScales = function() {
// Map of scale ID to scale object so we can lookup later
this.chartInstance.scales = {};
// Build the x axes
helpers.each(this.chartInstance.options.scales.xAxes, function(xAxisOptions) {
var ScaleClass = Chart.scaleService.getScaleConstructor(xAxisOptions.type);
var scale = new ScaleClass({
ctx: this.chartInstance.chart.ctx,
options: xAxisOptions,
data: this.chartInstance.data,
id: xAxisOptions.id,
});
this.chartInstance.scales[scale.id] = scale;
}, this);
// Build the y axes
helpers.each(this.chartInstance.options.scales.yAxes, function(yAxisOptions) {
var ScaleClass = Chart.scaleService.getScaleConstructor(yAxisOptions.type);
var scale = new ScaleClass({
ctx: this.chartInstance.chart.ctx,
options: yAxisOptions,
data: this.chartInstance.data,
id: yAxisOptions.id,
});
this.chartInstance.scales[scale.id] = scale;
}, this);
};
Chart.RectangularCanvasController.prototype.update = function() {
Chart.scaleService.fitScalesForChart(this.chartInstance, this.chartInstance.chart.width, this.chartInstance.chart.height);
this.elementController.updateElements();
};
}).call(this);

View File

@ -1,209 +0,0 @@
(function() {
"use strict";
//Declare root variable - window in the browser, global on the server
var root = this,
previous = root.Chart,
helpers = Chart.helpers;
//Create a dictionary of chart types, to allow for extension of existing types
Chart.types = {};
//Store a reference to each instance - allowing us to globally resize chart instances on window resize.
//Destroy method on the chart will remove the instance of the chart from this reference.
Chart.instances = {};
Chart.Type = function(config, instance) {
this.data = config.data;
this.options = config.options;
this.chart = instance;
this.id = helpers.uid();
//Add the chart instance to the global namespace
Chart.instances[this.id] = this;
// Initialize is always called when a chart type is created
// By default it is a no op, but it should be extended
if (this.options.responsive) {
this.resize();
}
this.initialize.call(this);
};
//Core methods that'll be a part of every chart type
helpers.extend(Chart.Type.prototype, {
initialize: function() {
return this;
},
clear: function() {
helpers.clear(this.chart);
return this;
},
stop: function() {
// Stops any current animation loop occuring
Chart.animationService.cancelAnimation(this);
return this;
},
resize: function() {
this.stop();
var canvas = this.chart.canvas,
newWidth = helpers.getMaximumWidth(this.chart.canvas),
newHeight = this.options.maintainAspectRatio ? newWidth / this.chart.aspectRatio : getMaximumHeight(this.chart.canvas);
canvas.width = this.chart.width = newWidth;
canvas.height = this.chart.height = newHeight;
helpers.retinaScale(this.chart);
return this;
},
update: function(animationDuration) {
this.canvasController.update();
this.render(animationDuration);
},
render: function(duration) {
if (this.options.animation.duration !== 0 || duration) {
var animation = new Chart.Animation();
animation.numSteps = (duration || this.options.animation.duration) / 16.66; //60 fps
animation.easing = this.options.animation.easing;
// render function
animation.render = function(chartInstance, animationObject) {
var easingFunction = helpers.easingEffects[animationObject.easing];
var stepDecimal = animationObject.currentStep / animationObject.numSteps;
var easeDecimal = easingFunction(stepDecimal);
chartInstance.draw(easeDecimal, stepDecimal, animationObject.currentStep);
};
// user events
animation.onAnimationProgress = this.options.onAnimationProgress;
animation.onAnimationComplete = this.options.onAnimationComplete;
Chart.animationService.addAnimation(this, animation, duration);
} else {
this.draw();
this.options.onAnimationComplete.call(this);
}
return this;
},
eachElement: function(callback) {
helpers.each(this.data.datasets, function(dataset, datasetIndex) {
helpers.each(dataset.metaData, callback, this, dataset.metaData, datasetIndex);
}, this);
},
eachValue: function(callback) {
helpers.each(this.data.datasets, function(dataset, datasetIndex) {
helpers.each(dataset.data, callback, this, datasetIndex);
}, this);
},
eachDataset: function(callback) {
helpers.each(this.data.datasets, callback, this);
},
getElementsAtEvent: function(e) {
var elementsArray = [],
eventPosition = helpers.getRelativePosition(e),
datasetIterator = function(dataset) {
elementsArray.push(dataset.metaData[elementIndex]);
},
elementIndex;
for (var datasetIndex = 0; datasetIndex < this.data.datasets.length; datasetIndex++) {
for (elementIndex = 0; elementIndex < this.data.datasets[datasetIndex].metaData.length; elementIndex++) {
if (this.data.datasets[datasetIndex].metaData[elementIndex].inGroupRange(eventPosition.x, eventPosition.y)) {
helpers.each(this.data.datasets, datasetIterator);
}
}
}
return elementsArray.length ? elementsArray : [];
},
// Get the single element that was clicked on
// @return : An object containing the dataset index and element index of the matching element. Also contains the rectangle that was drawn
getElementAtEvent: function(e) {
var element = [];
var eventPosition = helpers.getRelativePosition(e);
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].inRange(eventPosition.x, eventPosition.y)) {
element.push(this.data.datasets[datasetIndex].metaData[elementIndex]);
return element;
}
}
}
return [];
},
generateLegend: function() {
return template(this.options.legendTemplate, this);
},
destroy: function() {
this.clear();
unbindEvents(this, this.events);
var canvas = this.chart.canvas;
// Reset canvas height/width attributes starts a fresh with the canvas context
canvas.width = this.chart.width;
canvas.height = this.chart.height;
// < IE9 doesn't support removeProperty
if (canvas.style.removeProperty) {
canvas.style.removeProperty('width');
canvas.style.removeProperty('height');
} else {
canvas.style.removeAttribute('width');
canvas.style.removeAttribute('height');
}
delete Chart.instances[this.id];
},
toBase64Image: function() {
return this.chart.canvas.toDataURL.apply(this.chart.canvas, arguments);
}
});
Chart.Type.extend = function(extensions) {
var parent = this;
var ChartType = function() {
return parent.apply(this, arguments);
};
//Copy the prototype object of the this class
ChartType.prototype = helpers.clone(parent.prototype);
//Now overwrite some of the properties in the base class with the new extensions
helpers.extend(ChartType.prototype, extensions);
ChartType.extend = Chart.Type.extend;
if (extensions.name || parent.prototype.name) {
var chartName = extensions.name || parent.prototype.name;
//Assign any potential default values of the new chart type
//If none are defined, we'll use a clone of the chart type this is being extended from.
//I.e. if we extend a line chart, we'll use the defaults from the line chart if our new chart
//doesn't define some defaults of their own.
var baseDefaults = (Chart.defaults[parent.prototype.name]) ? helpers.clone(Chart.defaults[parent.prototype.name]) : {};
Chart.defaults[chartName] = helpers.configMerge(baseDefaults, extensions.defaults);
Chart.types[chartName] = ChartType;
//Register this new chart type in the Chart prototype
Chart.prototype[chartName] = function(config) {
config.options = helpers.configMerge(Chart.defaults.global, Chart.defaults[chartName], config.options || {});
return new ChartType(config, this);
};
} else {
warn("Name not provided for this chart, so it hasn't been registered");
}
return parent;
};
}).call(this);

371
src/core/core.controller.js Normal file
View File

@ -0,0 +1,371 @@
(function() {
"use strict";
//Declare root variable - window in the browser, global on the server
var root = this,
previous = root.Chart,
helpers = Chart.helpers;
//Create a dictionary of chart types, to allow for extension of existing types
Chart.types = {};
//Store a reference to each instance - allowing us to globally resize chart instances on window resize.
//Destroy method on the chart will remove the instance of the chart from this reference.
Chart.instances = {};
Chart.Controller = function(instance) {
this.chart = instance;
var config = instance.config;
this.data = config.data;
this.options = config.options = helpers.configMerge(Chart.defaults.global, Chart.defaults[config.type], config.options || {});
this.id = helpers.uid();
console.log(this.options);
//Add the chart instance to the global namespace
Chart.instances[this.id] = this;
// Initialize is always called when a chart type is created
// By default it is a no op, but it should be extended
if (this.options.responsive) {
this.resize();
}
this.initialize.call(this);
return this;
};
helpers.extend(Chart.Controller.prototype, {
initialize: function initialize() {
return this;
},
clear: function clear() {
helpers.clear(this.chart);
return this;
},
stop: function stop() {
// Stops any current animation loop occuring
Chart.animationService.cancelAnimation(this);
return this;
},
resize: function resize() {
this.stop();
var canvas = this.chart.canvas,
newWidth = helpers.getMaximumWidth(this.chart.canvas),
newHeight = this.options.maintainAspectRatio ? newWidth / this.chart.aspectRatio : getMaximumHeight(this.chart.canvas);
canvas.width = this.chart.width = newWidth;
canvas.height = this.chart.height = newHeight;
helpers.retinaScale(this.chart);
return this;
},
update: function update(animationDuration) {
this.canvasController.update();
this.render(animationDuration);
},
render: function render(duration) {
if (this.options.animation.duration !== 0 || duration) {
var animation = new Chart.Animation();
animation.numSteps = (duration || this.options.animation.duration) / 16.66; //60 fps
animation.easing = this.options.animation.easing;
// render function
animation.render = function(chartInstance, animationObject) {
var easingFunction = helpers.easingEffects[animationObject.easing];
var stepDecimal = animationObject.currentStep / animationObject.numSteps;
var easeDecimal = easingFunction(stepDecimal);
chartInstance.draw(easeDecimal, stepDecimal, animationObject.currentStep);
};
// user events
animation.onAnimationProgress = this.options.onAnimationProgress;
animation.onAnimationComplete = this.options.onAnimationComplete;
Chart.animationService.addAnimation(this, animation, duration);
} else {
this.draw();
this.options.onAnimationComplete.call(this);
}
return this;
},
eachElement: function eachElement(callback) {
helpers.each(this.data.datasets, function(dataset, datasetIndex) {
helpers.each(dataset.metaData, callback, this, dataset.metaData, datasetIndex);
}, this);
},
eachValue: function eachValue(callback) {
helpers.each(this.data.datasets, function(dataset, datasetIndex) {
helpers.each(dataset.data, callback, this, datasetIndex);
}, this);
},
eachDataset: function eachDataset(callback) {
helpers.each(this.data.datasets, callback, this);
},
getElementsAtEvent: function getElementsAtEvent(e) {
var elementsArray = [],
eventPosition = helpers.getRelativePosition(e),
datasetIterator = function(dataset) {
elementsArray.push(dataset.metaData[elementIndex]);
},
elementIndex;
for (var datasetIndex = 0; datasetIndex < this.data.datasets.length; datasetIndex++) {
for (elementIndex = 0; elementIndex < this.data.datasets[datasetIndex].metaData.length; elementIndex++) {
if (this.data.datasets[datasetIndex].metaData[elementIndex].inGroupRange(eventPosition.x, eventPosition.y)) {
helpers.each(this.data.datasets, datasetIterator);
}
}
}
return elementsArray.length ? elementsArray : [];
},
// Get the single element that was clicked on
// @return : An object containing the dataset index and element index of the matching element. Also contains the rectangle that was draw
getElementAtEvent: function getElementAtEvent(e) {
var element = [];
var eventPosition = helpers.getRelativePosition(e);
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].inRange(eventPosition.x, eventPosition.y)) {
element.push(this.data.datasets[datasetIndex].metaData[elementIndex]);
return element;
}
}
}
return [];
},
generateLegend: function generateLegend() {
return template(this.options.legendTemplate, this);
},
destroy: function destroy() {
this.clear();
unbindEvents(this, this.events);
var canvas = this.chart.canvas;
// Reset canvas height/width attributes starts a fresh with the canvas context
canvas.width = this.chart.width;
canvas.height = this.chart.height;
// < IE9 doesn't support removeProperty
if (canvas.style.removeProperty) {
canvas.style.removeProperty('width');
canvas.style.removeProperty('height');
} else {
canvas.style.removeAttribute('width');
canvas.style.removeAttribute('height');
}
delete Chart.instances[this.id];
},
toBase64Image: function toBase64Image() {
return this.chart.canvas.toDataURL.apply(this.chart.canvas, arguments);
},
initialize: function initialize() {
this.bindEvents();
this.buildScales();
// Need to fit scales before we reset elements.
Chart.scaleService.fitScalesForChart(this, this.chart.width, this.chart.height);
this.elementController.resetElements();
this.initToolTip();
this.update();
},
bindEvents: function bindEvents() {
helpers.bindEvents(this, this.options.events, function(evt) {
// this will be the chart instance
this.canvasController.eventHandler(evt);
});
},
eventHandler: function eventHandler(e) {
this.lastActive = this.lastActive || [];
// Find Active Elements
if (e.type == 'mouseout') {
this.active = [];
} else {
this.active = function() {
switch (this.options.hover.mode) {
case 'single':
return this.elementController.getElementAtEvent(e);
case 'label':
return this.elementController.getElementsAtEvent(e);
case 'dataset':
return this.elementController.getDatasetAtEvent(e);
default:
return e;
}
}.call(this);
}
// On Hover hook
if (this.options.hover.onHover) {
this.options.hover.onHover.call(this, this.active);
}
if (e.type == 'mouseup' || e.type == 'click') {
if (this.options.onClick) {
this.options.onClick.call(this, e, this.active);
}
}
var dataset;
var index;
// Remove styling for last active (even if it may still be active)
if (this.lastActive.length) {
switch (this.options.hover.mode) {
case 'single':
this.elementController.resetElementAppearance(this.lastActive[0], this.lastActive[0]._datasetIndex, this.lastActive[0]._index);
break;
case 'label':
for (var i = 0; i < this.lastActive.length; i++) {
this.elementController.resetElementAppearance(this.lastActive[i], this.lastActive[i]._datasetIndex, this.lastActive[i]._index);
}
break;
case 'dataset':
break;
default:
// Don't change anything
}
}
// Built in hover styling
if (this.active.length && this.options.hover.mode) {
switch (this.options.hover.mode) {
case 'single':
this.elementController.setElementHoverStyle(this.active[0]);
break;
case 'label':
for (var i = 0; i < this.active.length; i++) {
this.elementController.setElementHoverStyle(this.active[i]);
}
break;
case 'dataset':
break;
default:
// Don't change anything
}
}
// Built in Tooltips
if (this.options.tooltips.enabled) {
// The usual updates
this.tooltip.initialize();
// Active
if (this.active.length) {
this.tooltip._model.opacity = 1;
helpers.extend(this.tooltip, {
_active: this.active,
});
this.tooltip.update();
} else {
// Inactive
this.tooltip._model.opacity = 0;
}
}
// Hover animations
this.tooltip.pivot();
if (!this.animating) {
var changed;
helpers.each(this.active, function(element, index) {
if (element !== this.lastActive[index]) {
changed = true;
}
}, this);
// If entering, leaving, or changing elements, animate the change via pivot
if ((!this.lastActive.length && this.active.length) ||
(this.lastActive.length && !this.active.length) ||
(this.lastActive.length && this.active.length && changed)) {
this.stop();
this.render(this.options.hover.animationDuration);
}
}
// Remember Last Active
this.lastActive = this.active;
return this;
},
initToolTip: function initToolTip() {
this.tooltip = new Chart.Tooltip({
_chart: this.chart,
_data: this.data,
_options: this.options,
}, this);
},
buildScales: function buildScales() {
// Map of scale ID to scale object so we can lookup later
this.scales = {};
// Build the x axes
helpers.each(this.options.scales.xAxes, function(xAxisOptions) {
var ScaleClass = Chart.scaleService.getScaleConstructor(xAxisOptions.type);
var scale = new ScaleClass({
ctx: this.chart.ctx,
options: xAxisOptions,
data: this.data,
id: xAxisOptions.id,
});
this.scales[scale.id] = scale;
}, this);
// Build the y axes
helpers.each(this.options.scales.yAxes, function(yAxisOptions) {
var ScaleClass = Chart.scaleService.getScaleConstructor(yAxisOptions.type);
var scale = new ScaleClass({
ctx: this.chart.ctx,
options: yAxisOptions,
data: this.data,
id: yAxisOptions.id,
});
this.scales[scale.id] = scale;
}, this);
},
update: function update() {
Chart.scaleService.fitScalesForChart(this, this.chart.width, this.chart.height);
this.elementController.updateElements();
}
});
}).call(this);

View File

@ -18,8 +18,9 @@
previous = root.Chart;
//Occupy the global variable of Chart, and create a simple base class
var Chart = function(context) {
var Chart = function(context, config) {
var chart = this;
this.config = config;
// Support a jQuery'd canvas element
if (context.length && context[0].getContext) {
@ -57,7 +58,9 @@
//High pixel density displays - multiply the size of the canvas height/width by the device pixel ratio, then scale.
Chart.helpers.retinaScale(this);
return this;
this.controller = new Chart.Controller(this);
return this.controller;
};
var defaultColor = 'rgba(0,0,0,0.1)';