Refactoring to put browser specific code in a new class, BrowserPlatform.
BrowserPlatform implements IPlatform. Chart.Platform is the constructor for the platform object that is attached to the chart instance.
Plugins are notified about the event using the `onEvent` call. The legend plugin was converted to use onEvent instead of the older private `handleEvent` method.
Wrote test to check that plugins are notified about events
* After datasets draw
* Resize
* Before an animation is started
+* When an event occurs on the canvas (mousemove, click, etc). This requires the `options.events` property handled
Plugins should derive from Chart.PluginBase and implement the following interface
```javascript
afterDatasetsDraw: function(chartInstance, easing) { },
destroy: function(chartInstance) { }
+
+ /**
+ * Called when an event occurs on the chart
+ * @param e {Core.Event} the Chart.js wrapper around the native event. e.native is the original event
+ * @return {Boolean} true if the chart is changed and needs to re-render
+ */
+ onEvent: function(chartInstance, e) {}
}
```
require('./core/core.interaction')(Chart);
require('./core/core.tooltip')(Chart);
+// By default, we only load the browser platform.
+Chart.platform = require('./platforms/platform.dom')(Chart);
+
require('./elements/element.arc')(Chart);
require('./elements/element.line')(Chart);
require('./elements/element.point')(Chart);
// Controllers available for dataset visualization eg. bar, line, slice, etc.
Chart.controllers = {};
- /**
- * The "used" size is the final value of a dimension property after all calculations have
- * been performed. This method uses the computed style of `element` but returns undefined
- * if the computed style is not expressed in pixels. That can happen in some cases where
- * `element` has a size relative to its parent and this last one is not yet displayed,
- * for example because of `display: none` on a parent node.
- * TODO(SB) Move this method in the upcoming core.platform class.
- * @see https://developer.mozilla.org/en-US/docs/Web/CSS/used_value
- * @returns {Number} Size in pixels or undefined if unknown.
- */
- function readUsedSize(element, property) {
- var value = helpers.getStyle(element, property);
- var matches = value && value.match(/(\d+)px/);
- return matches? Number(matches[1]) : undefined;
- }
-
- /**
- * Initializes the canvas style and render size without modifying the canvas display size,
- * since responsiveness is handled by the controller.resize() method. The config is used
- * to determine the aspect ratio to apply in case no explicit height has been specified.
- * TODO(SB) Move this method in the upcoming core.platform class.
- */
- function initCanvas(canvas, config) {
- var style = canvas.style;
-
- // NOTE(SB) canvas.getAttribute('width') !== canvas.width: in the first case it
- // returns null or '' if no explicit value has been set to the canvas attribute.
- var renderHeight = canvas.getAttribute('height');
- var renderWidth = canvas.getAttribute('width');
-
- // Chart.js modifies some canvas values that we want to restore on destroy
- canvas._chartjs = {
- initial: {
- height: renderHeight,
- width: renderWidth,
- style: {
- display: style.display,
- height: style.height,
- width: style.width
- }
- }
- };
-
- // Force canvas to display as block to avoid extra space caused by inline
- // elements, which would interfere with the responsive resize process.
- // https://github.com/chartjs/Chart.js/issues/2538
- style.display = style.display || 'block';
-
- if (renderWidth === null || renderWidth === '') {
- var displayWidth = readUsedSize(canvas, 'width');
- if (displayWidth !== undefined) {
- canvas.width = displayWidth;
- }
- }
-
- if (renderHeight === null || renderHeight === '') {
- if (canvas.style.height === '') {
- // If no explicit render height and style height, let's apply the aspect ratio,
- // which one can be specified by the user but also by charts as default option
- // (i.e. options.aspectRatio). If not specified, use canvas aspect ratio of 2.
- canvas.height = canvas.width / (config.options.aspectRatio || 2);
- } else {
- var displayHeight = readUsedSize(canvas, 'height');
- if (displayWidth !== undefined) {
- canvas.height = displayHeight;
- }
- }
- }
-
- return canvas;
- }
-
- /**
- * Restores the canvas initial state, such as render/display sizes and style.
- * TODO(SB) Move this method in the upcoming core.platform class.
- */
- function releaseCanvas(canvas) {
- if (!canvas._chartjs) {
- return;
- }
-
- var initial = canvas._chartjs.initial;
- ['height', 'width'].forEach(function(prop) {
- var value = initial[prop];
- if (value === undefined || value === null) {
- canvas.removeAttribute(prop);
- } else {
- canvas.setAttribute(prop, value);
- }
- });
-
- helpers.each(initial.style || {}, function(value, key) {
- canvas.style[key] = value;
- });
-
- // The canvas render size might have been changed (and thus the state stack discarded),
- // we can't use save() and restore() to restore the initial state. So make sure that at
- // least the canvas context is reset to the default state by setting the canvas width.
- // https://www.w3.org/TR/2011/WD-html5-20110525/the-canvas-element.html
- canvas.width = canvas.width;
-
- delete canvas._chartjs;
- }
-
- /**
- * TODO(SB) Move this method in the upcoming core.platform class.
- */
- function acquireContext(item, config) {
- if (typeof item === 'string') {
- item = document.getElementById(item);
- } else if (item.length) {
- // Support for array based queries (such as jQuery)
- item = item[0];
- }
-
- if (item && item.canvas) {
- // Support for any object associated to a canvas (including a context2d)
- item = item.canvas;
- }
-
- if (item instanceof HTMLCanvasElement) {
- // To prevent canvas fingerprinting, some add-ons undefine the getContext
- // method, for example: https://github.com/kkapsner/CanvasBlocker
- // https://github.com/chartjs/Chart.js/issues/2807
- var context = item.getContext && item.getContext('2d');
- if (context instanceof CanvasRenderingContext2D) {
- initCanvas(item, config);
- return context;
- }
- }
-
- return null;
- }
-
/**
* Initializes the given config with global and chart default values.
*/
config = initConfig(config);
- var context = acquireContext(item, config);
+ var context = Chart.platform.acquireContext(item, config);
var canvas = context && context.canvas;
var height = canvas && canvas.height;
var width = canvas && canvas.width;
helpers.unbindEvents(me, me.events);
helpers.removeResizeListener(canvas.parentNode);
helpers.clear(me.chart);
- releaseCanvas(canvas);
+ Chart.platform.releaseContext(me.chart.ctx);
me.chart.canvas = null;
me.chart.ctx = null;
}
eventHandler: function(e) {
var me = this;
- var legend = me.legend;
var tooltip = me.tooltip;
var hoverOptions = me.options.hover;
me._bufferedRender = true;
me._bufferedRequest = null;
- var changed = me.handleEvent(e);
- changed |= legend && legend.handleEvent(e);
- changed |= tooltip && tooltip.handleEvent(e);
+ // Create platform agnostic chart event using platform specific code
+ var chartEvent = Chart.platform.createEvent(e, me.chart);
+
+ var changed = me.handleEvent(chartEvent);
+ changed |= tooltip && tooltip.handleEvent(chartEvent);
+ changed |= Chart.plugins.notify(me, 'onEvent', [chartEvent]);
var bufferedRequest = me._bufferedRequest;
if (bufferedRequest) {
/**
* Handle an event
* @private
- * param e {Event} the event to handle
+ * param e {Core.Event} the event to handle
* @return {Boolean} true if the chart needs to re-render
*/
handleEvent: function(e) {
// On Hover hook
if (hoverOptions.onHover) {
- hoverOptions.onHover.call(me, e, me.active);
+ // Need to call with native event here to not break backwards compatibility
+ hoverOptions.onHover.call(me, e.native, me.active);
}
if (e.type === 'mouseup' || e.type === 'click') {
if (options.onClick) {
- options.onClick.call(me, e, me.active);
+ // Use e.native here for backwards compatibility
+ options.onClick.call(me, e.native, me.active);
}
}
module.exports = function(Chart) {
var helpers = Chart.helpers;
+ /**
+ * Helper function to get relative position for an event
+ * @param e {Event|Core.Event} the event to get the position for
+ * @param chart {chart} the chart
+ * @returns {Point} the event position
+ */
+ function getRelativePosition(e, chart) {
+ if (e.native) {
+ return {
+ x: e.x,
+ y: e.y
+ };
+ }
+
+ return helpers.getRelativePosition(e, chart);
+ }
+
/**
* Helper function to traverse all of the visible elements in the chart
* @param chart {chart} the chart
}
function indexMode(chart, e, options) {
- var position = helpers.getRelativePosition(e, chart.chart);
+ var position = getRelativePosition(e, chart.chart);
var distanceMetric = function(pt1, pt2) {
return Math.abs(pt1.x - pt2.x);
};
// Helper function for different modes
modes: {
single: function(chart, e) {
- var position = helpers.getRelativePosition(e, chart.chart);
+ var position = getRelativePosition(e, chart.chart);
var elements = [];
parseVisibleItems(chart, function(element) {
* @return {Chart.Element[]} Array of elements that are under the point. If none are found, an empty array is returned
*/
dataset: function(chart, e, options) {
- var position = helpers.getRelativePosition(e, chart.chart);
+ var position = getRelativePosition(e, chart.chart);
var items = options.intersect ? getIntersectItems(chart, position) : getNearestItems(chart, position, false);
if (items.length > 0) {
* @return {Chart.Element[]} Array of elements that are under the point. If none are found, an empty array is returned
*/
point: function(chart, e) {
- var position = helpers.getRelativePosition(e, chart.chart);
+ var position = getRelativePosition(e, chart.chart);
return getIntersectItems(chart, position);
},
* @return {Chart.Element[]} Array of elements that are under the point. If none are found, an empty array is returned
*/
nearest: function(chart, e, options) {
- var position = helpers.getRelativePosition(e, chart.chart);
+ var position = getRelativePosition(e, chart.chart);
var nearestItems = getNearestItems(chart, position, options.intersect);
// We have multiple items at the same distance from the event. Now sort by smallest
* @return {Chart.Element[]} Array of elements that are under the point. If none are found, an empty array is returned
*/
x: function(chart, e, options) {
- var position = helpers.getRelativePosition(e, chart.chart);
+ var position = getRelativePosition(e, chart.chart);
var items = [];
var intersectsItem = false;
* @return {Chart.Element[]} Array of elements that are under the point. If none are found, an empty array is returned
*/
y: function(chart, e, options) {
- var position = helpers.getRelativePosition(e, chart.chart);
+ var position = getRelativePosition(e, chart.chart);
var items = [];
var intersectsItem = false;
/**
* Handle an event
* @private
- * @param e {Event} the event to handle
+ * @param e {Core.Event} the event to handle
* @return {Boolean} true if a change occured
*/
handleEvent: function(e) {
return;
}
- var position = helpers.getRelativePosition(e, me.chart.chart),
- x = position.x,
- y = position.y;
+ // Chart event already has relative position in it
+ var x = e.x,
+ y = e.y;
if (x >= me.left && x <= me.right && y >= me.top && y <= me.bottom) {
// See if we are touching one of the dataset boxes
if (x >= hitBox.left && x <= hitBox.left + hitBox.width && y >= hitBox.top && y <= hitBox.top + hitBox.height) {
// Touching an element
if (type === 'click') {
- opts.onClick.call(me, e, me.legendItems[i]);
+ // use e.native for backwards compatibility
+ opts.onClick.call(me, e.native, me.legendItems[i]);
changed = true;
break;
} else if (type === 'mousemove') {
- opts.onHover.call(me, e, me.legendItems[i]);
+ // use e.native for backwards compatibility
+ opts.onHover.call(me, e.native, me.legendItems[i]);
changed = true;
break;
}
Chart.layoutService.removeBox(chartInstance, chartInstance.legend);
delete chartInstance.legend;
}
+ },
+ onEvent: function(chartInstance, e) {
+ var legend = chartInstance.legend;
+ if (legend) {
+ legend.handleEvent(e);
+ }
}
});
};
/**
* Handle an event
* @private
- * @param e {Event} the event to handle
+ * @param e {Core.Event} the event to handle
* @returns {Boolean} true if the tooltip changed
*/
handleEvent: function(e) {
me._lastActive = me._active;
if (options.enabled || options.custom) {
- me._eventPosition = helpers.getRelativePosition(e, me._chart);
+ me._eventPosition = {
+ x: e.x,
+ y: e.y
+ };
var model = me._model;
me.update(true);
--- /dev/null
+'use strict';
+
+/**
+ * @interface IPlatform
+ * Allows abstracting platform dependencies away from the chart
+ */
+/**
+ * Creates a chart.js event from a platform specific event
+ * @method IPlatform#createEvent
+ * @param e {Event} : the platform event to translate
+ * @returns {Core.Event} chart.js event
+ */
+/**
+ * @method IPlatform#acquireContext
+ * @param item {Object} the context or canvas to use
+ * @param config {ChartOptions} the chart options
+ * @returns {CanvasRenderingContext2D} a context2d instance implementing the w3c Canvas 2D context API standard.
+ */
+/**
+ * @method IPlatform#releaseContext
+ * @param context {CanvasRenderingContext2D} the context to release. This is the item returned by @see {@link IPlatform#acquireContext}
+ */
+
+// Chart.Platform implementation for targeting a web browser
+module.exports = function(Chart) {
+ var helpers = Chart.helpers;
+
+ /*
+ * Key is the browser event type
+ * Chart.js internal events are:
+ * mouseenter
+ * mousedown
+ * mousemove
+ * mouseup
+ * mouseout
+ * click
+ * dblclick
+ * contextmenu
+ * keydown
+ * keypress
+ * keyup
+ */
+ var typeMap = {
+ // Mouse events
+ mouseenter: 'mouseenter',
+ mousedown: 'mousedown',
+ mousemove: 'mousemove',
+ mouseup: 'mouseup',
+ mouseout: 'mouseout',
+ mouseleave: 'mouseout',
+ click: 'click',
+ dblclick: 'dblclick',
+ contextmenu: 'contextmenu',
+
+ // Touch events
+ touchstart: 'mousedown',
+ touchmove: 'mousemove',
+ touchend: 'mouseup',
+
+ // Pointer events
+ pointerenter: 'mouseenter',
+ pointerdown: 'mousedown',
+ pointermove: 'mousemove',
+ pointerup: 'mouseup',
+ pointerleave: 'mouseout',
+ pointerout: 'mouseout',
+
+ // Key events
+ keydown: 'keydown',
+ keypress: 'keypress',
+ keyup: 'keyup',
+ };
+
+ /**
+ * The "used" size is the final value of a dimension property after all calculations have
+ * been performed. This method uses the computed style of `element` but returns undefined
+ * if the computed style is not expressed in pixels. That can happen in some cases where
+ * `element` has a size relative to its parent and this last one is not yet displayed,
+ * for example because of `display: none` on a parent node.
+ * @see https://developer.mozilla.org/en-US/docs/Web/CSS/used_value
+ * @returns {Number} Size in pixels or undefined if unknown.
+ */
+ function readUsedSize(element, property) {
+ var value = helpers.getStyle(element, property);
+ var matches = value && value.match(/(\d+)px/);
+ return matches? Number(matches[1]) : undefined;
+ }
+
+ /**
+ * Initializes the canvas style and render size without modifying the canvas display size,
+ * since responsiveness is handled by the controller.resize() method. The config is used
+ * to determine the aspect ratio to apply in case no explicit height has been specified.
+ */
+ function initCanvas(canvas, config) {
+ var style = canvas.style;
+
+ // NOTE(SB) canvas.getAttribute('width') !== canvas.width: in the first case it
+ // returns null or '' if no explicit value has been set to the canvas attribute.
+ var renderHeight = canvas.getAttribute('height');
+ var renderWidth = canvas.getAttribute('width');
+
+ // Chart.js modifies some canvas values that we want to restore on destroy
+ canvas._chartjs = {
+ initial: {
+ height: renderHeight,
+ width: renderWidth,
+ style: {
+ display: style.display,
+ height: style.height,
+ width: style.width
+ }
+ }
+ };
+
+ // Force canvas to display as block to avoid extra space caused by inline
+ // elements, which would interfere with the responsive resize process.
+ // https://github.com/chartjs/Chart.js/issues/2538
+ style.display = style.display || 'block';
+
+ if (renderWidth === null || renderWidth === '') {
+ var displayWidth = readUsedSize(canvas, 'width');
+ if (displayWidth !== undefined) {
+ canvas.width = displayWidth;
+ }
+ }
+
+ if (renderHeight === null || renderHeight === '') {
+ if (canvas.style.height === '') {
+ // If no explicit render height and style height, let's apply the aspect ratio,
+ // which one can be specified by the user but also by charts as default option
+ // (i.e. options.aspectRatio). If not specified, use canvas aspect ratio of 2.
+ canvas.height = canvas.width / (config.options.aspectRatio || 2);
+ } else {
+ var displayHeight = readUsedSize(canvas, 'height');
+ if (displayWidth !== undefined) {
+ canvas.height = displayHeight;
+ }
+ }
+ }
+
+ return canvas;
+ }
+
+ return {
+ /**
+ * Creates a Chart.js event from a raw event
+ * @method BrowserPlatform#createEvent
+ * @implements IPlatform.createEvent
+ * @param e {Event} the raw event (such as a mouse event)
+ * @param chart {Chart} the chart to use
+ * @returns {Core.Event} the chart.js event for this event
+ */
+ createEvent: function(e, chart) {
+ var relativePosition = helpers.getRelativePosition(e, chart);
+ return {
+ // allow access to the native event
+ native: e,
+
+ // our interal event type
+ type: typeMap[e.type],
+
+ // width and height of chart
+ width: chart.width,
+ height: chart.height,
+
+ // Position relative to the canvas
+ x: relativePosition.x,
+ y: relativePosition.y
+ };
+ },
+
+ /**
+ * @method BrowserPlatform#acquireContext
+ * @implements IPlatform#acquireContext
+ */
+ acquireContext: function(item, config) {
+ if (typeof item === 'string') {
+ item = document.getElementById(item);
+ } else if (item.length) {
+ // Support for array based queries (such as jQuery)
+ item = item[0];
+ }
+
+ if (item && item.canvas) {
+ // Support for any object associated to a canvas (including a context2d)
+ item = item.canvas;
+ }
+
+ if (item instanceof HTMLCanvasElement) {
+ // To prevent canvas fingerprinting, some add-ons undefine the getContext
+ // method, for example: https://github.com/kkapsner/CanvasBlocker
+ // https://github.com/chartjs/Chart.js/issues/2807
+ var context = item.getContext && item.getContext('2d');
+ if (context instanceof CanvasRenderingContext2D) {
+ initCanvas(item, config);
+ return context;
+ }
+ }
+
+ return null;
+ },
+
+ /**
+ * Restores the canvas initial state, such as render/display sizes and style.
+ * @method BrowserPlatform#releaseContext
+ * @implements IPlatform#releaseContext
+ */
+ releaseContext: function(context) {
+ var canvas = context.canvas;
+ if (!canvas._chartjs) {
+ return;
+ }
+
+ var initial = canvas._chartjs.initial;
+ ['height', 'width'].forEach(function(prop) {
+ var value = initial[prop];
+ if (value === undefined || value === null) {
+ canvas.removeAttribute(prop);
+ } else {
+ canvas.setAttribute(prop, value);
+ }
+ });
+
+ helpers.each(initial.style || {}, function(value, key) {
+ canvas.style[key] = value;
+ });
+
+ // The canvas render size might have been changed (and thus the state stack discarded),
+ // we can't use save() and restore() to restore the initial state. So make sure that at
+ // least the canvas context is reset to the default state by setting the canvas width.
+ // https://www.w3.org/TR/2011/WD-html5-20110525/the-canvas-element.html
+ canvas.width = canvas.width;
+
+ delete canvas._chartjs;
+ }
+ };
+};
Chart.helpers.addEvent(content, state !== 'complete'? 'load' : 'resize', handler);
}
- describe('context acquisition', function() {
- var canvasId = 'chartjs-canvas';
-
- beforeEach(function() {
- var canvas = document.createElement('canvas');
- canvas.setAttribute('id', canvasId);
- window.document.body.appendChild(canvas);
- });
-
- afterEach(function() {
- document.getElementById(canvasId).remove();
- });
-
- // see https://github.com/chartjs/Chart.js/issues/2807
- it('should gracefully handle invalid item', function() {
- var chart = new Chart('foobar');
-
- expect(chart).not.toBeValidChart();
-
- chart.destroy();
- });
-
- it('should accept a DOM element id', function() {
- var canvas = document.getElementById(canvasId);
- var chart = new Chart(canvasId);
-
- expect(chart).toBeValidChart();
- expect(chart.chart.canvas).toBe(canvas);
- expect(chart.chart.ctx).toBe(canvas.getContext('2d'));
-
- chart.destroy();
- });
-
- it('should accept a canvas element', function() {
- var canvas = document.getElementById(canvasId);
- var chart = new Chart(canvas);
-
- expect(chart).toBeValidChart();
- expect(chart.chart.canvas).toBe(canvas);
- expect(chart.chart.ctx).toBe(canvas.getContext('2d'));
-
- chart.destroy();
- });
-
- it('should accept a canvas context2D', function() {
- var canvas = document.getElementById(canvasId);
- var context = canvas.getContext('2d');
- var chart = new Chart(context);
-
- expect(chart).toBeValidChart();
- expect(chart.chart.canvas).toBe(canvas);
- expect(chart.chart.ctx).toBe(context);
-
- chart.destroy();
- });
-
- it('should accept an array containing canvas', function() {
- var canvas = document.getElementById(canvasId);
- var chart = new Chart([canvas]);
-
- expect(chart).toBeValidChart();
- expect(chart.chart.canvas).toBe(canvas);
- expect(chart.chart.ctx).toBe(canvas.getContext('2d'));
-
- chart.destroy();
- });
- });
-
describe('config initialization', function() {
it('should create missing config.data properties', function() {
var chart = acquireChart({});
});
});
- describe('config.options.aspectRatio', function() {
- it('should use default "global" aspect ratio for render and display sizes', function() {
- var chart = acquireChart({
- options: {
- responsive: false
- }
- }, {
- canvas: {
- style: 'width: 620px'
- }
- });
-
- expect(chart).toBeChartOfSize({
- dw: 620, dh: 310,
- rw: 620, rh: 310,
- });
- });
-
- it('should use default "chart" aspect ratio for render and display sizes', function() {
- var chart = acquireChart({
- type: 'doughnut',
- options: {
- responsive: false
- }
- }, {
- canvas: {
- style: 'width: 425px'
- }
- });
-
- expect(chart).toBeChartOfSize({
- dw: 425, dh: 425,
- rw: 425, rh: 425,
- });
- });
-
- it('should use "user" aspect ratio for render and display sizes', function() {
- var chart = acquireChart({
- options: {
- responsive: false,
- aspectRatio: 3
- }
- }, {
- canvas: {
- style: 'width: 405px'
- }
- });
-
- expect(chart).toBeChartOfSize({
- dw: 405, dh: 135,
- rw: 405, rh: 135,
- });
- });
-
- it('should not apply aspect ratio when height specified', function() {
- var chart = acquireChart({
- options: {
- responsive: false,
- aspectRatio: 3
- }
- }, {
- canvas: {
- style: 'width: 400px; height: 410px'
- }
- });
-
- expect(chart).toBeChartOfSize({
- dw: 400, dh: 410,
- rw: 400, rh: 410,
- });
- });
- });
-
describe('config.options.responsive: false', function() {
- it('should use default canvas size for render and display sizes', function() {
- var chart = acquireChart({
- options: {
- responsive: false
- }
- }, {
- canvas: {
- style: ''
- }
- });
-
- expect(chart).toBeChartOfSize({
- dw: 300, dh: 150,
- rw: 300, rh: 150,
- });
- });
-
- it('should use canvas attributes for render and display sizes', function() {
- var chart = acquireChart({
- options: {
- responsive: false
- }
- }, {
- canvas: {
- style: '',
- width: 305,
- height: 245,
- }
- });
-
- expect(chart).toBeChartOfSize({
- dw: 305, dh: 245,
- rw: 305, rh: 245,
- });
- });
-
- it('should use canvas style for render and display sizes (if no attributes)', function() {
- var chart = acquireChart({
- options: {
- responsive: false
- }
- }, {
- canvas: {
- style: 'width: 345px; height: 125px'
- }
- });
-
- expect(chart).toBeChartOfSize({
- dw: 345, dh: 125,
- rw: 345, rh: 125,
- });
- });
-
- it('should use attributes for the render size and style for the display size', function() {
- var chart = acquireChart({
- options: {
- responsive: false
- }
- }, {
- canvas: {
- style: 'width: 345px; height: 125px;',
- width: 165,
- height: 85,
- }
- });
-
- expect(chart).toBeChartOfSize({
- dw: 345, dh: 125,
- rw: 165, rh: 85,
- });
- });
-
it('should not inject the resizer element', function() {
var chart = acquireChart({
options: {
});
describe('config.options.responsive: true (maintainAspectRatio: true)', function() {
- it('should fill parent width and use aspect ratio to calculate height', function() {
- var chart = acquireChart({
- options: {
- responsive: true,
- maintainAspectRatio: true
- }
- }, {
- canvas: {
- style: 'width: 150px; height: 245px'
- },
- wrapper: {
- style: 'width: 300px; height: 350px'
- }
- });
-
- expect(chart).toBeChartOfSize({
- dw: 300, dh: 490,
- rw: 300, rh: 490,
- });
- });
-
it('should resize the canvas with correct aspect ratio when parent width changes', function(done) {
var chart = acquireChart({
type: 'line', // AR == 2
});
describe('controller.destroy', function() {
- it('should reset context to default values', function() {
- var chart = acquireChart({});
- var context = chart.chart.ctx;
-
- chart.destroy();
-
- // https://www.w3.org/TR/2dcontext/#conformance-requirements
- Chart.helpers.each({
- fillStyle: '#000000',
- font: '10px sans-serif',
- lineJoin: 'miter',
- lineCap: 'butt',
- lineWidth: 1,
- miterLimit: 10,
- shadowBlur: 0,
- shadowColor: 'rgba(0, 0, 0, 0)',
- shadowOffsetX: 0,
- shadowOffsetY: 0,
- strokeStyle: '#000000',
- textAlign: 'start',
- textBaseline: 'alphabetic'
- }, function(value, key) {
- expect(context[key]).toBe(value);
- });
- });
-
- it('should restore canvas initial values', function(done) {
- var chart = acquireChart({
- options: {
- responsive: true,
- maintainAspectRatio: false
- }
- }, {
- canvas: {
- width: 180,
- style: 'width: 512px; height: 480px'
- },
- wrapper: {
- style: 'width: 450px; height: 450px; position: relative'
- }
- });
-
- var canvas = chart.chart.canvas;
- var wrapper = canvas.parentNode;
- wrapper.style.width = '475px';
- waitForResize(chart, function() {
- expect(chart).toBeChartOfSize({
- dw: 475, dh: 450,
- rw: 475, rh: 450,
- });
-
- chart.destroy();
-
- expect(canvas.getAttribute('width')).toBe('180');
- expect(canvas.getAttribute('height')).toBe(null);
- expect(canvas.style.width).toBe('512px');
- expect(canvas.style.height).toBe('480px');
- expect(canvas.style.display).toBe('');
-
- done();
- });
- });
-
it('should remove the resizer element when responsive: true', function() {
var chart = acquireChart({
options: {
--- /dev/null
+describe('Platform.dom', function() {
+
+ function waitForResize(chart, callback) {
+ var resizer = chart.chart.canvas.parentNode._chartjs.resizer;
+ var content = resizer.contentWindow || resizer;
+ var state = content.document.readyState || 'complete';
+ var handler = function() {
+ Chart.helpers.removeEvent(content, 'load', handler);
+ Chart.helpers.removeEvent(content, 'resize', handler);
+ setTimeout(callback, 50);
+ };
+
+ Chart.helpers.addEvent(content, state !== 'complete'? 'load' : 'resize', handler);
+ }
+
+ describe('context acquisition', function() {
+ var canvasId = 'chartjs-canvas';
+
+ beforeEach(function() {
+ var canvas = document.createElement('canvas');
+ canvas.setAttribute('id', canvasId);
+ window.document.body.appendChild(canvas);
+ });
+
+ afterEach(function() {
+ document.getElementById(canvasId).remove();
+ });
+
+ // see https://github.com/chartjs/Chart.js/issues/2807
+ it('should gracefully handle invalid item', function() {
+ var chart = new Chart('foobar');
+
+ expect(chart).not.toBeValidChart();
+
+ chart.destroy();
+ });
+
+ it('should accept a DOM element id', function() {
+ var canvas = document.getElementById(canvasId);
+ var chart = new Chart(canvasId);
+
+ expect(chart).toBeValidChart();
+ expect(chart.chart.canvas).toBe(canvas);
+ expect(chart.chart.ctx).toBe(canvas.getContext('2d'));
+
+ chart.destroy();
+ });
+
+ it('should accept a canvas element', function() {
+ var canvas = document.getElementById(canvasId);
+ var chart = new Chart(canvas);
+
+ expect(chart).toBeValidChart();
+ expect(chart.chart.canvas).toBe(canvas);
+ expect(chart.chart.ctx).toBe(canvas.getContext('2d'));
+
+ chart.destroy();
+ });
+
+ it('should accept a canvas context2D', function() {
+ var canvas = document.getElementById(canvasId);
+ var context = canvas.getContext('2d');
+ var chart = new Chart(context);
+
+ expect(chart).toBeValidChart();
+ expect(chart.chart.canvas).toBe(canvas);
+ expect(chart.chart.ctx).toBe(context);
+
+ chart.destroy();
+ });
+
+ it('should accept an array containing canvas', function() {
+ var canvas = document.getElementById(canvasId);
+ var chart = new Chart([canvas]);
+
+ expect(chart).toBeValidChart();
+ expect(chart.chart.canvas).toBe(canvas);
+ expect(chart.chart.ctx).toBe(canvas.getContext('2d'));
+
+ chart.destroy();
+ });
+ });
+
+ describe('config.options.aspectRatio', function() {
+ it('should use default "global" aspect ratio for render and display sizes', function() {
+ var chart = acquireChart({
+ options: {
+ responsive: false
+ }
+ }, {
+ canvas: {
+ style: 'width: 620px'
+ }
+ });
+
+ expect(chart).toBeChartOfSize({
+ dw: 620, dh: 310,
+ rw: 620, rh: 310,
+ });
+ });
+
+ it('should use default "chart" aspect ratio for render and display sizes', function() {
+ var chart = acquireChart({
+ type: 'doughnut',
+ options: {
+ responsive: false
+ }
+ }, {
+ canvas: {
+ style: 'width: 425px'
+ }
+ });
+
+ expect(chart).toBeChartOfSize({
+ dw: 425, dh: 425,
+ rw: 425, rh: 425,
+ });
+ });
+
+ it('should use "user" aspect ratio for render and display sizes', function() {
+ var chart = acquireChart({
+ options: {
+ responsive: false,
+ aspectRatio: 3
+ }
+ }, {
+ canvas: {
+ style: 'width: 405px'
+ }
+ });
+
+ expect(chart).toBeChartOfSize({
+ dw: 405, dh: 135,
+ rw: 405, rh: 135,
+ });
+ });
+
+ it('should not apply aspect ratio when height specified', function() {
+ var chart = acquireChart({
+ options: {
+ responsive: false,
+ aspectRatio: 3
+ }
+ }, {
+ canvas: {
+ style: 'width: 400px; height: 410px'
+ }
+ });
+
+ expect(chart).toBeChartOfSize({
+ dw: 400, dh: 410,
+ rw: 400, rh: 410,
+ });
+ });
+ });
+
+ describe('config.options.responsive: false', function() {
+ it('should use default canvas size for render and display sizes', function() {
+ var chart = acquireChart({
+ options: {
+ responsive: false
+ }
+ }, {
+ canvas: {
+ style: ''
+ }
+ });
+
+ expect(chart).toBeChartOfSize({
+ dw: 300, dh: 150,
+ rw: 300, rh: 150,
+ });
+ });
+
+ it('should use canvas attributes for render and display sizes', function() {
+ var chart = acquireChart({
+ options: {
+ responsive: false
+ }
+ }, {
+ canvas: {
+ style: '',
+ width: 305,
+ height: 245,
+ }
+ });
+
+ expect(chart).toBeChartOfSize({
+ dw: 305, dh: 245,
+ rw: 305, rh: 245,
+ });
+ });
+
+ it('should use canvas style for render and display sizes (if no attributes)', function() {
+ var chart = acquireChart({
+ options: {
+ responsive: false
+ }
+ }, {
+ canvas: {
+ style: 'width: 345px; height: 125px'
+ }
+ });
+
+ expect(chart).toBeChartOfSize({
+ dw: 345, dh: 125,
+ rw: 345, rh: 125,
+ });
+ });
+
+ it('should use attributes for the render size and style for the display size', function() {
+ var chart = acquireChart({
+ options: {
+ responsive: false
+ }
+ }, {
+ canvas: {
+ style: 'width: 345px; height: 125px;',
+ width: 165,
+ height: 85,
+ }
+ });
+
+ expect(chart).toBeChartOfSize({
+ dw: 345, dh: 125,
+ rw: 165, rh: 85,
+ });
+ });
+ });
+
+ describe('config.options.responsive: true (maintainAspectRatio: true)', function() {
+ it('should fill parent width and use aspect ratio to calculate height', function() {
+ var chart = acquireChart({
+ options: {
+ responsive: true,
+ maintainAspectRatio: true
+ }
+ }, {
+ canvas: {
+ style: 'width: 150px; height: 245px'
+ },
+ wrapper: {
+ style: 'width: 300px; height: 350px'
+ }
+ });
+
+ expect(chart).toBeChartOfSize({
+ dw: 300, dh: 490,
+ rw: 300, rh: 490,
+ });
+ });
+ });
+
+ describe('controller.destroy', function() {
+ it('should reset context to default values', function() {
+ var chart = acquireChart({});
+ var context = chart.chart.ctx;
+
+ chart.destroy();
+
+ // https://www.w3.org/TR/2dcontext/#conformance-requirements
+ Chart.helpers.each({
+ fillStyle: '#000000',
+ font: '10px sans-serif',
+ lineJoin: 'miter',
+ lineCap: 'butt',
+ lineWidth: 1,
+ miterLimit: 10,
+ shadowBlur: 0,
+ shadowColor: 'rgba(0, 0, 0, 0)',
+ shadowOffsetX: 0,
+ shadowOffsetY: 0,
+ strokeStyle: '#000000',
+ textAlign: 'start',
+ textBaseline: 'alphabetic'
+ }, function(value, key) {
+ expect(context[key]).toBe(value);
+ });
+ });
+
+ it('should restore canvas initial values', function(done) {
+ var chart = acquireChart({
+ options: {
+ responsive: true,
+ maintainAspectRatio: false
+ }
+ }, {
+ canvas: {
+ width: 180,
+ style: 'width: 512px; height: 480px'
+ },
+ wrapper: {
+ style: 'width: 450px; height: 450px; position: relative'
+ }
+ });
+
+ var canvas = chart.chart.canvas;
+ var wrapper = canvas.parentNode;
+ wrapper.style.width = '475px';
+ waitForResize(chart, function() {
+ expect(chart).toBeChartOfSize({
+ dw: 475, dh: 450,
+ rw: 475, rh: 450,
+ });
+
+ chart.destroy();
+
+ expect(canvas.getAttribute('width')).toBe('180');
+ expect(canvas.getAttribute('height')).toBe(null);
+ expect(canvas.style.width).toBe('512px');
+ expect(canvas.style.height).toBe('480px');
+ expect(canvas.style.display).toBe('');
+
+ done();
+ });
+ });
+ });
+
+ describe('event handling', function() {
+ it('should notify plugins about events', function() {
+ var notifiedEvent;
+ var plugin = {
+ onEvent: function(chart, e) {
+ notifiedEvent = e;
+ }
+ };
+ var chart = acquireChart({
+ type: 'line',
+ data: {
+ labels: ['A', 'B', 'C', 'D'],
+ datasets: [{
+ data: [10, 20, 30, 100]
+ }]
+ },
+ options: {
+ responsive: true
+ },
+ plugins: [plugin]
+ });
+
+ var node = chart.chart.canvas;
+ var rect = node.getBoundingClientRect();
+ var clientX = (rect.left + rect.right) / 2;
+ var clientY = (rect.top + rect.bottom) / 2;
+
+ var evt = new MouseEvent('click', {
+ view: window,
+ bubbles: true,
+ cancelable: true,
+ clientX: clientX,
+ clientY: clientY
+ });
+
+ // Manually trigger rather than having an async test
+ node.dispatchEvent(evt);
+
+ // Check that notifiedEvent is correct
+ expect(notifiedEvent).not.toBe(undefined);
+ expect(notifiedEvent.native).toBe(evt);
+
+ // Is type correctly translated
+ expect(notifiedEvent.type).toBe(evt.type);
+
+ // Canvas width and height
+ expect(notifiedEvent.width).toBe(chart.chart.width);
+ expect(notifiedEvent.height).toBe(chart.chart.height);
+
+ // Relative Position
+ expect(notifiedEvent.x).toBe(chart.chart.width / 2);
+ expect(notifiedEvent.y).toBe(chart.chart.height / 2);
+ });
+ });
+});