From b59d81f486cc8e523b6087990252e1728a820c2b Mon Sep 17 00:00:00 2001 From: Simon Brunel Date: Wed, 16 Aug 2017 16:45:21 +0200 Subject: [PATCH 1/3] Introduce scriptable options (bubble chart) New `options.resolve` helper that determines the final value to use from an array of input values (fallback) and a given context and/or index. For now, only the bubble chart support scriptable options, see documentation for details. Add scriptable options documentation and update the bubble chart dataset properties table with their scriptable and indexable capabilities and default values. Also move point style description under the element configuration section. --- docs/SUMMARY.md | 3 +- docs/charts/bubble.md | 68 ++++-- docs/charts/line.md | 31 +-- docs/configuration/elements.md | 18 +- docs/general/README.md | 13 +- docs/general/options.md | 48 +++++ samples/samples.js | 6 + samples/scriptable/bubble.html | 128 ++++++++++++ samples/utils.js | 25 ++- src/controllers/controller.bubble.js | 152 +++++++++----- src/helpers/helpers.options.js | 32 ++- test/specs/controller.bubble.tests.js | 284 +++++++++++++------------- test/specs/helpers.options.tests.js | 61 ++++++ 13 files changed, 620 insertions(+), 249 deletions(-) create mode 100644 docs/general/options.md create mode 100644 samples/scriptable/bubble.html diff --git a/docs/SUMMARY.md b/docs/SUMMARY.md index 041be7062af..60630090003 100644 --- a/docs/SUMMARY.md +++ b/docs/SUMMARY.md @@ -10,6 +10,7 @@ * [Interactions](general/interactions/README.md) * [Events](general/interactions/events.md) * [Modes](general/interactions/modes.md) + * [Options](general/options.md) * [Colors](general/colors.md) * [Fonts](general/fonts.md) * [Configuration](configuration/README.md) @@ -34,7 +35,7 @@ * [Category](axes/cartesian/category.md) * [Linear](axes/cartesian/linear.md) * [Logarithmic](axes/cartesian/logarithmic.md) - * [Time](axes/cartesian/time.md) + * [Time](axes/cartesian/time.md) * [Radial](axes/radial/README.md) * [Linear](axes/radial/linear.md) * [Labelling](axes/labelling.md) diff --git a/docs/charts/bubble.md b/docs/charts/bubble.md index df96911c601..4cb2ee6994f 100644 --- a/docs/charts/bubble.md +++ b/docs/charts/bubble.md @@ -1,6 +1,6 @@ # Bubble Chart -A bubble chart is used to display three dimensions of data at the same time. The location of the bubble is determined by the first two dimensions and the corresponding horizontal and vertical axes. The third dimension is represented by the size of the individual bubbles. +A bubble chart is used to display three dimensions of data at the same time. The location of the bubble is determined by the first two dimensions and the corresponding horizontal and vertical axes. The third dimension is represented by the size of the individual bubbles. {% chartjs %} { @@ -38,22 +38,52 @@ var myBubbleChart = new Chart(ctx,{ The bubble chart allows a number of properties to be specified for each dataset. These are used to set display properties for a specific dataset. For example, the colour of the bubbles is generally set this way. -All properties, except `label` can be specified as an array. If these are set to an array value, the first value applies to the first bubble in the dataset, the second value to the second bubble, and so on. +| Name | Type | [Scriptable](../general/options.md#scriptable-options) | [Indexable](../general/options.md#indexable-options) | Default +| ---- | ---- | :----: | :----: | ---- +| [`backgroundColor`](#styling) | [`Color`](../general/colors.md) | Yes | Yes | `'rgba(0,0,0,0.1)'` +| [`borderColor`](#styling) | [`Color`](../general/colors.md) | Yes | Yes | `'rgba(0,0,0,0.1)'` +| [`borderWidth`](#styling) | `Number` | Yes | Yes | `3` +| [`data`](#data-structure) | `Object[]` | - | - | **required** +| [`hoverBackgroundColor`](#interactions) | [`Color`](../general/colors.md) | Yes | Yes | `undefined` +| [`hoverBorderColor`](#interactions) | [`Color`](../general/colors.md) | Yes | Yes | `undefined` +| [`hoverBorderWidth`](#interactions) | `Number` | Yes | Yes | `1` +| [`hoverRadius`](#interactions) | `Number` | Yes | Yes | `4` +| [`hitRadius`](#interactions) | `Number` | Yes | Yes | `1` +| [`label`](#labeling) | `String` | - | - | `undefined` +| [`pointStyle`](#styling) | `String` | Yes | Yes | `circle` +| [`radius`](#styling) | `Number` | Yes | Yes | `3` -| Name | Type | Description -| ---- | ---- | ----------- -| `label` | `String` | The label for the dataset which appears in the legend and tooltips. -| `backgroundColor` | `Color/Color[]` | The fill color for bubbles. -| `borderColor` | `Color/Color[]` | The border color for bubbles. -| `borderWidth` | `Number/Number[]` | The width of the point bubbles in pixels. -| `hoverBackgroundColor` | `Color/Color[]` | Bubble background color when hovered. -| `hoverBorderColor` | `Color/Color[]` | Bubble border color when hovered. -| `hoverBorderWidth` | `Number/Number[]` | Border width of point when hovered. -| `hoverRadius` | `Number/Number[]` | Additional radius to add to data radius on hover. +### Labeling -## Config Options +`label` defines the text associated to the dataset and which appears in the legend and tooltips. -The bubble chart has no unique configuration options. To configure options common to all of the bubbles, the [point element options](../configuration/elements.md#point-configuration) are used. +### Styling + +The style of each bubble can be controlled with the following properties: + +| Name | Description +| ---- | ---- +| `backgroundColor` | bubble background color +| `borderColor` | bubble border color +| `borderWidth` | bubble border width (in pixels) +| `pointStyle` | bubble [shape style](../configuration/elements#point-styles) +| `radius` | bubble radius (in pixels) + +All these values, if `undefined`, fallback to the associated [`elements.point.*`](../configuration/elements.md#point-configuration) options. + +### Interactions + +The interaction with each bubble can be controlled with the following properties: + +| Name | Description +| ---- | ----------- +| `hoverBackgroundColor` | bubble background color when hovered +| `hoverBorderColor` | bubble border color hovered +| `hoverBorderWidth` | bubble border width when hovered (in pixels) +| `hoverRadius` | bubble **additional** radius when hovered (in pixels) +| `hitRadius` | bubble **additional** radius for hit detection (in pixels) + +All these values, if `undefined`, fallback to the associated [`elements.point.*`](../configuration/elements.md#point-configuration) options. ## Default Options @@ -61,11 +91,7 @@ We can also change the default values for the Bubble chart type. Doing so will g ## Data Structure -For a bubble chart, datasets need to contain an array of data points. Each point must implement the [bubble data object](#bubble-data-object) interface. - -### Bubble Data Object - -Data for the bubble chart is passed in the form of an object. The object must implement the following interface. It is important to note that the radius property, `r` is **not** scaled by the chart. It is the raw radius in pixels of the bubble that is drawn on the canvas. +Bubble chart datasets need to contain a `data` array of points, each points represented by an object containing the following properties: ```javascript { @@ -75,7 +101,9 @@ Data for the bubble chart is passed in the form of an object. The object must im // Y Value y: , - // Radius of bubble. This is not scaled. + // Bubble radius in pixels (not scaled). r: } ``` + +**Important:** the radius property, `r` is **not** scaled by the chart, it is the raw radius in pixels of the bubble that is drawn on the canvas. diff --git a/docs/charts/line.md b/docs/charts/line.md index 9d0901be04c..cd4141a49a0 100644 --- a/docs/charts/line.md +++ b/docs/charts/line.md @@ -6,11 +6,11 @@ A line chart is a way of plotting data points on a line. Often, it is used to sh "type": "line", "data": { "labels": [ - "January", - "February", - "March", - "April", - "May", + "January", + "February", + "March", + "April", + "May", "June", "July" ], @@ -62,7 +62,7 @@ All point* properties can be specified as an array. If these are set to an array | `pointBorderColor` | `Color/Color[]` | The border color for points. | `pointBorderWidth` | `Number/Number[]` | The width of the point border in pixels. | `pointRadius` | `Number/Number[]` | The radius of the point shape. If set to 0, the point is not rendered. -| `pointStyle` | `String/String[]/Image/Image[]` | Style of the point. [more...](#pointstyle) +| `pointStyle` | `String/String[]/Image/Image[]` | Style of the point. [more...](../configuration/elements#point-styles) | `pointHitRadius` | `Number/Number[]` | The pixel size of the non-displayed point that reacts to mouse events. | `pointHoverBackgroundColor` | `Color/Color[]` | Point background color when hovered. | `pointHoverBorderColor` | `Color/Color[]` | Point border color when hovered. @@ -75,7 +75,7 @@ All point* properties can be specified as an array. If these are set to an array ### cubicInterpolationMode The following interpolation modes are supported: * 'default' -* 'monotone'. +* 'monotone'. The 'default' algorithm uses a custom weighted cubic interpolation, which produces pleasant curves for all types of datasets. @@ -83,21 +83,6 @@ The 'monotone' algorithm is more suited to `y = f(x)` datasets : it preserves mo If left untouched (`undefined`), the global `options.elements.line.cubicInterpolationMode` property is used. -### pointStyle -The style of point. Options are: -* 'circle' -* 'cross' -* 'crossRot' -* 'dash'. -* 'line' -* 'rect' -* 'rectRounded' -* 'rectRot' -* 'star' -* 'triangle' - -If the option is an image, that image is drawn on the canvas using [drawImage](https://developer.mozilla.org/en/docs/Web/API/CanvasRenderingContext2D/drawImage). - ### Stepped Line The following values are supported for `steppedLine`: * `false`: No Step Interpolation (default) @@ -127,7 +112,7 @@ Chart.defaults.line.spanGaps = true; ## Data Structure -The `data` property of a dataset for a line chart can be passed in two formats. +The `data` property of a dataset for a line chart can be passed in two formats. ### Number[] ```javascript diff --git a/docs/configuration/elements.md b/docs/configuration/elements.md index 0586a14b2c4..5375a7e9f1f 100644 --- a/docs/configuration/elements.md +++ b/docs/configuration/elements.md @@ -18,7 +18,7 @@ Global point options: `Chart.defaults.global.elements.point` | Name | Type | Default | Description | -----| ---- | --------| ----------- | `radius` | `Number` | `3` | Point radius. -| `pointStyle` | `String` | `circle` | Point style. +| [`pointStyle`](#point-styles) | `String` | `circle` | Point style. | `backgroundColor` | `Color` | `'rgba(0,0,0,0.1)'` | Point fill color. | `borderWidth` | `Number` | `1` | Point stroke width. | `borderColor` | `Color` | `'rgba(0,0,0,0.1)'` | Point stroke color. @@ -26,6 +26,22 @@ Global point options: `Chart.defaults.global.elements.point` | `hoverRadius` | `Number` | `4` | Point radius when hovered. | `hoverBorderWidth` | `Number` | `1` | Stroke width when hovered. +### Point Styles + +The following values are supported: +- `'circle'` +- `'cross'` +- `'crossRot'` +- `'dash'` +- `'line'` +- `'rect'` +- `'rectRounded'` +- `'rectRot'` +- `'star'` +- `'triangle'` + +If the value is an image, that image is drawn on the canvas using [drawImage](https://developer.mozilla.org/en/docs/Web/API/CanvasRenderingContext2D/drawImage). + ## Line Configuration Line elements are used to represent the line in a line chart. diff --git a/docs/general/README.md b/docs/general/README.md index 76ec5c6ba1c..950188ff1d5 100644 --- a/docs/general/README.md +++ b/docs/general/README.md @@ -1,9 +1,10 @@ -# General Chart.js Configuration +# General Configuration -These sections describe general configuration options that can apply elsewhere in the documentation. +These sections describe general configuration options that can apply elsewhere in the documentation. -* [Colors](./colors.md) defines acceptable color values -* [Font](./fonts.md) defines various font options -* [Interactions](./interactions/README.md) defines options that reflect how hovering chart elements works * [Responsive](./responsive.md) defines responsive chart options that apply to all charts. -* [Device Pixel Ratio](./device-pixel-ratio.md) defines the ratio between display pixels and rendered pixels \ No newline at end of file +* [Device Pixel Ratio](./device-pixel-ratio.md) defines the ratio between display pixels and rendered pixels. +* [Interactions](./interactions/README.md) defines options that reflect how hovering chart elements works. +* [Options](./options.md) scriptable and indexable options syntax. +* [Colors](./colors.md) defines acceptable color values. +* [Font](./fonts.md) defines various font options. diff --git a/docs/general/options.md b/docs/general/options.md new file mode 100644 index 00000000000..f0b8746596b --- /dev/null +++ b/docs/general/options.md @@ -0,0 +1,48 @@ +# Options + +## Scriptable Options + +Scriptable options also accept a function which is called for each data and that takes the unique argument `context` representing contextual information (see [option context](options.md#option-context)). + +Example: + +```javascript +color: function(context) { + return context.data < 0 ? 'red' : // draw negative values in red + index%2 ? 'blue' : 'green'; // else, alternate values in blue and green +} +``` + +> **Note:** scriptable options are only supported by a few bubble chart options. + +## Indexable Options + +Indexable options also accept an array in which each item corresponds to the element at the same index. Note that this method requires to provide as many items as data, so, in most cases, using a [function](#scriptable-options) is more appropriated if supported. + +Example: + +```javascript +color: [ + 'red', // color for data at index 0 + 'blue', // color for data at index 1 + 'green', // color for data at index 2 + 'black', // color for data at index 3 + //... +] +``` + +## Option Context + +The option context is used to give contextual information when resolving options and currently only applies to [scriptable options](#scriptable-options). + +The context object contains the following properties: + +- `datasetIndex`: index of the current dataset +- `datasetCount`: number of datasets +- `dataset`: dataset at index `datasetIndex` +- `dataIndex`: index of the current data +- `dataCount`: number of data +- `data`: value of the current data +- `chart`: the associated chart + +**Important**: since the context can represent different types of entities (dataset, data, etc.), some properties may be `undefined` so be sure to test any context property before using it. diff --git a/samples/samples.js b/samples/samples.js index 235685bf2fa..e7147f5ef38 100644 --- a/samples/samples.js +++ b/samples/samples.js @@ -167,6 +167,12 @@ title: 'HTML tooltips (points)', path: 'tooltips/custom-points.html' }] + }, { + title: 'Scriptable', + items: [{ + title: 'Bubble Chart', + path: 'scriptable/bubble.html' + }] }, { title: 'Advanced', items: [{ diff --git a/samples/scriptable/bubble.html b/samples/scriptable/bubble.html new file mode 100644 index 00000000000..4f80c6089ed --- /dev/null +++ b/samples/scriptable/bubble.html @@ -0,0 +1,128 @@ + + + + + + + Scriptable > Bubble | Chart.js sample + + + + + +
+
+
+ + + +
+
+ + + + diff --git a/samples/utils.js b/samples/utils.js index b6ea8d7b95d..0b31703a1f1 100644 --- a/samples/utils.js +++ b/samples/utils.js @@ -12,10 +12,6 @@ window.chartColors = { grey: 'rgb(201, 203, 207)' }; -window.randomScalingFactor = function() { - return (Math.random() > 0.5 ? 1.0 : -1.0) * Math.round(Math.random() * 100); -}; - (function(global) { var Months = [ 'January', @@ -32,6 +28,18 @@ window.randomScalingFactor = function() { 'December' ]; + var COLORS = [ + '#4dc9f6', + '#f67019', + '#f53794', + '#537bc4', + '#acc236', + '#166a8f', + '#00a950', + '#58595b', + '#8549ba' + ]; + var Samples = global.Samples || (global.Samples = {}); Samples.utils = { // Adapted from http://indiegamr.com/generate-repeatable-random-numbers-in-js/ @@ -105,6 +113,10 @@ window.randomScalingFactor = function() { return values; }, + color: function(index) { + return COLORS[index % COLORS.length]; + }, + transparentize: function(color, opacity) { var alpha = opacity === undefined ? 0.5 : 1 - opacity; return Chart.helpers.color(color).alpha(alpha).rgbString(); @@ -115,5 +127,10 @@ window.randomScalingFactor = function() { Samples.utils.srand(Date.now()); + // DEPRECATED + window.randomScalingFactor = function() { + return Math.round(Samples.utils.rand(-100, 100)); + }; + }(this)); diff --git a/src/controllers/controller.bubble.js b/src/controllers/controller.bubble.js index be229da9def..2fbaf7401d4 100644 --- a/src/controllers/controller.bubble.js +++ b/src/controllers/controller.bubble.js @@ -41,9 +41,14 @@ defaults._set('bubble', { module.exports = function(Chart) { Chart.controllers.bubble = Chart.DatasetController.extend({ - + /** + * @protected + */ dataElementType: elements.Point, + /** + * @protected + */ update: function(reset) { var me = this; var meta = me.getMeta(); @@ -55,71 +60,124 @@ module.exports = function(Chart) { }); }, + /** + * @protected + */ updateElement: function(point, index, reset) { var me = this; var meta = me.getMeta(); + var custom = point.custom || {}; var xScale = me.getScaleForId(meta.xAxisID); var yScale = me.getScaleForId(meta.yAxisID); - - var custom = point.custom || {}; - var dataset = me.getDataset(); - var data = dataset.data[index]; - var pointElementOptions = me.chart.options.elements.point; + var options = me._resolveElementOptions(point, index); + var data = me.getDataset().data[index]; var dsIndex = me.index; - helpers.extend(point, { - // Utility - _xScale: xScale, - _yScale: yScale, - _datasetIndex: dsIndex, - _index: index, - - // Desired view properties - _model: { - x: reset ? xScale.getPixelForDecimal(0.5) : xScale.getPixelForValue(typeof data === 'object' ? data : NaN, index, dsIndex), - y: reset ? yScale.getBasePixel() : yScale.getPixelForValue(data, index, dsIndex), - // Appearance - radius: reset ? 0 : custom.radius ? custom.radius : me.getRadius(data), - - // Tooltip - hitRadius: custom.hitRadius ? custom.hitRadius : helpers.valueAtIndexOrDefault(dataset.hitRadius, index, pointElementOptions.hitRadius) - } - }); - - // Trick to reset the styles of the point - Chart.DatasetController.prototype.removeHoverStyle.call(me, point, pointElementOptions); - - var model = point._model; - model.skip = custom.skip ? custom.skip : (isNaN(model.x) || isNaN(model.y)); + var x = reset ? xScale.getPixelForDecimal(0.5) : xScale.getPixelForValue(typeof data === 'object' ? data : NaN, index, dsIndex); + var y = reset ? yScale.getBasePixel() : yScale.getPixelForValue(data, index, dsIndex); + + point._xScale = xScale; + point._yScale = yScale; + point._options = options; + point._datasetIndex = dsIndex; + point._index = index; + point._model = { + backgroundColor: options.backgroundColor, + borderColor: options.borderColor, + borderWidth: options.borderWidth, + hitRadius: options.hitRadius, + pointStyle: options.pointStyle, + radius: reset ? 0 : options.radius, + skip: custom.skip || isNaN(x) || isNaN(y), + x: x, + y: y, + }; point.pivot(); }, - getRadius: function(value) { - return value.r || this.chart.options.elements.point.radius; - }, - + /** + * @protected + */ setHoverStyle: function(point) { - var me = this; - Chart.DatasetController.prototype.setHoverStyle.call(me, point); - - // Radius - var dataset = me.chart.data.datasets[point._datasetIndex]; - var index = point._index; - var custom = point.custom || {}; var model = point._model; - model.radius = custom.hoverRadius ? custom.hoverRadius : (helpers.valueAtIndexOrDefault(dataset.hoverRadius, index, me.chart.options.elements.point.hoverRadius)) + me.getRadius(dataset.data[index]); + var options = point._options; + + model.backgroundColor = helpers.valueOrDefault(options.hoverBackgroundColor, helpers.getHoverColor(options.backgroundColor)); + model.borderColor = helpers.valueOrDefault(options.hoverBorderColor, helpers.getHoverColor(options.borderColor)); + model.borderWidth = helpers.valueOrDefault(options.hoverBorderWidth, options.borderWidth); + model.radius = options.radius + options.hoverRadius; }, + /** + * @protected + */ removeHoverStyle: function(point) { - var me = this; - Chart.DatasetController.prototype.removeHoverStyle.call(me, point, me.chart.options.elements.point); + var model = point._model; + var options = point._options; + + model.backgroundColor = options.backgroundColor; + model.borderColor = options.borderColor; + model.borderWidth = options.borderWidth; + model.radius = options.radius; + }, - var dataVal = me.chart.data.datasets[point._datasetIndex].data[point._index]; + /** + * @private + */ + _resolveElementOptions: function(point, index) { + var me = this; + var chart = me.chart; + var datasets = chart.data.datasets; + var dataset = datasets[me.index]; var custom = point.custom || {}; - var model = point._model; + var options = chart.options.elements.point; + var resolve = helpers.options.resolve; + var data = dataset.data[index]; + var values = {}; + var i, ilen, key; + + // Scriptable options + var context = { + chart: chart, + datasetIndex: me.index, + datasetCount: datasets.length, + dataset: dataset, + dataIndex: index, + dataCount: dataset.data.length, + data: data + }; + + var keys = [ + 'backgroundColor', + 'borderColor', + 'borderWidth', + 'hoverBackgroundColor', + 'hoverBorderColor', + 'hoverBorderWidth', + 'hoverRadius', + 'hitRadius', + 'pointStyle' + ]; + + for (i = 0, ilen = keys.length; i < ilen; ++i) { + key = keys[i]; + values[key] = resolve([ + custom[key], + dataset[key], + options[key] + ], context, index); + } + + // Custom radius resolution + values.radius = resolve([ + custom.radius, + data ? data.r : undefined, + dataset.radius, + options.radius + ], context, index); - model.radius = custom.radius ? custom.radius : me.getRadius(dataVal); + return values; } }); }; diff --git a/src/helpers/helpers.options.js b/src/helpers/helpers.options.js index 2c9dc759b74..8e6c0aadfae 100644 --- a/src/helpers/helpers.options.js +++ b/src/helpers/helpers.options.js @@ -3,7 +3,8 @@ var helpers = require('./helpers.core'); /** - * @namespace Chart.helpers.options + * @alias Chart.helpers.options + * @namespace */ module.exports = { /** @@ -62,5 +63,34 @@ module.exports = { height: t + b, width: l + r }; + }, + + /** + * Evaluates the given `inputs` sequentially and returns the first defined value. + * @param {Array[]} inputs - An array of values, falling back to the last value. + * @param {Object} [context] - If defined and the current value is a function, the value + * is called with `context` as first argument and the result becomes the new input. + * @param {Number} [index] - If defined and the current value is an array, the value + * at `index` become the new input. + * @since 2.7.0 + */ + resolve: function(inputs, context, index) { + var i, ilen, value; + + for (i = 0, ilen = inputs.length; i < ilen; ++i) { + value = inputs[i]; + if (value === undefined) { + continue; + } + if (context !== undefined && typeof value === 'function') { + value = value(context); + } + if (index !== undefined && helpers.isArray(value)) { + value = value[index]; + } + if (value !== undefined) { + return value; + } + } } }; diff --git a/test/specs/controller.bubble.tests.js b/test/specs/controller.bubble.tests.js index 6212b0c3dc6..dce403d685a 100644 --- a/test/specs/controller.bubble.tests.js +++ b/test/specs/controller.bubble.tests.js @@ -273,160 +273,152 @@ describe('Bubble controller tests', function() { expect(meta.data[4] instanceof Chart.elements.Point).toBe(true); }); - it('should set hover styles', function() { - var chart = window.acquireChart({ - type: 'bubble', - data: { - datasets: [{ - data: [{ - x: 10, - y: 10, - r: 5 - }, { - x: -15, - y: -10, - r: 1 - }, { - x: 0, - y: -9, - r: 2 - }, { - x: -4, - y: 10, - r: 1 + describe('Interactions', function() { + function triggerElementEvent(chart, type, el) { + var node = chart.canvas; + var rect = node.getBoundingClientRect(); + var event = new MouseEvent(type, { + clientX: rect.left + el._model.x, + clientY: rect.top + el._model.y, + cancelable: true, + bubbles: true, + view: window + }); + + node.dispatchEvent(event); + } + + beforeEach(function() { + this.chart = window.acquireChart({ + type: 'bubble', + data: { + labels: ['label1', 'label2', 'label3', 'label4'], + datasets: [{ + data: [{ + x: 5, + y: 5, + r: 20 + }, { + x: -15, + y: -10, + r: 15 + }, { + x: 15, + y: 10, + r: 10 + }, { + x: -15, + y: 10, + r: 5 + }] }] - }], - labels: ['label1', 'label2', 'label3', 'label4'] - }, - options: { - elements: { - point: { - backgroundColor: 'rgb(255, 255, 0)', - borderWidth: 1, - borderColor: 'rgb(255, 255, 255)', - hitRadius: 1, - hoverRadius: 4, - hoverBorderWidth: 1, - radius: 3 + }, + options: { + elements: { + point: { + backgroundColor: 'rgb(100, 150, 200)', + borderColor: 'rgb(50, 100, 150)', + borderWidth: 2, + radius: 3 + } } } - } + }); }); - var meta = chart.getDatasetMeta(0); - var point = meta.data[0]; - - meta.controller.setHoverStyle(point); - expect(point._model.backgroundColor).toBe('rgb(229, 230, 0)'); - expect(point._model.borderColor).toBe('rgb(230, 230, 230)'); - expect(point._model.borderWidth).toBe(1); - expect(point._model.radius).toBe(9); - - // Can set hover style per dataset - chart.data.datasets[0].hoverRadius = 3.3; - chart.data.datasets[0].hoverBackgroundColor = 'rgb(77, 79, 81)'; - chart.data.datasets[0].hoverBorderColor = 'rgb(123, 125, 127)'; - chart.data.datasets[0].hoverBorderWidth = 2.1; - - meta.controller.setHoverStyle(point); - expect(point._model.backgroundColor).toBe('rgb(77, 79, 81)'); - expect(point._model.borderColor).toBe('rgb(123, 125, 127)'); - expect(point._model.borderWidth).toBe(2.1); - expect(point._model.radius).toBe(8.3); - - // Custom style - point.custom = { - hoverRadius: 4.4, - hoverBorderWidth: 5.5, - hoverBackgroundColor: 'rgb(0, 0, 0)', - hoverBorderColor: 'rgb(10, 10, 10)' - }; - - meta.controller.setHoverStyle(point); - expect(point._model.backgroundColor).toBe('rgb(0, 0, 0)'); - expect(point._model.borderColor).toBe('rgb(10, 10, 10)'); - expect(point._model.borderWidth).toBe(5.5); - expect(point._model.radius).toBe(4.4); - }); + it ('should handle default hover styles', function() { + var chart = this.chart; + var point = chart.getDatasetMeta(0).data[0]; + + triggerElementEvent(chart, 'mousemove', point); + expect(point._model.backgroundColor).toBe('rgb(49, 135, 221)'); + expect(point._model.borderColor).toBe('rgb(22, 89, 156)'); + expect(point._model.borderWidth).toBe(1); + expect(point._model.radius).toBe(20 + 4); + + triggerElementEvent(chart, 'mouseout', point); + expect(point._model.backgroundColor).toBe('rgb(100, 150, 200)'); + expect(point._model.borderColor).toBe('rgb(50, 100, 150)'); + expect(point._model.borderWidth).toBe(2); + expect(point._model.radius).toBe(20); + }); - it('should remove hover styles', function() { - var chart = window.acquireChart({ - type: 'bubble', - data: { - datasets: [{ - data: [{ - x: 10, - y: 10, - r: 5 - }, { - x: -15, - y: -10, - r: 1 - }, { - x: 0, - y: -9, - r: 2 - }, { - x: -4, - y: 10, - r: 1 - }] - }], - labels: ['label1', 'label2', 'label3', 'label4'] - }, - options: { - elements: { - point: { - backgroundColor: 'rgb(255, 255, 0)', - borderWidth: 1, - borderColor: 'rgb(255, 255, 255)', - hitRadius: 1, - hoverRadius: 4, - hoverBorderWidth: 1, - radius: 3 - } - } - } + it ('should handle hover styles defined via dataset properties', function() { + var chart = this.chart; + var point = chart.getDatasetMeta(0).data[0]; + + Chart.helpers.merge(chart.data.datasets[0], { + hoverBackgroundColor: 'rgb(200, 100, 150)', + hoverBorderColor: 'rgb(150, 50, 100)', + hoverBorderWidth: 8.4, + hoverRadius: 4.2 + }); + + chart.update(); + + triggerElementEvent(chart, 'mousemove', point); + expect(point._model.backgroundColor).toBe('rgb(200, 100, 150)'); + expect(point._model.borderColor).toBe('rgb(150, 50, 100)'); + expect(point._model.borderWidth).toBe(8.4); + expect(point._model.radius).toBe(20 + 4.2); + + triggerElementEvent(chart, 'mouseout', point); + expect(point._model.backgroundColor).toBe('rgb(100, 150, 200)'); + expect(point._model.borderColor).toBe('rgb(50, 100, 150)'); + expect(point._model.borderWidth).toBe(2); + expect(point._model.radius).toBe(20); }); - var meta = chart.getDatasetMeta(0); - var point = meta.data[0]; - - chart.options.elements.point.backgroundColor = 'rgb(45, 46, 47)'; - chart.options.elements.point.borderColor = 'rgb(50, 51, 52)'; - chart.options.elements.point.borderWidth = 10.1; - chart.options.elements.point.radius = 1.01; - - meta.controller.removeHoverStyle(point); - expect(point._model.backgroundColor).toBe('rgb(45, 46, 47)'); - expect(point._model.borderColor).toBe('rgb(50, 51, 52)'); - expect(point._model.borderWidth).toBe(10.1); - expect(point._model.radius).toBe(5); - - // Can set hover style per dataset - chart.data.datasets[0].radius = 3.3; - chart.data.datasets[0].backgroundColor = 'rgb(77, 79, 81)'; - chart.data.datasets[0].borderColor = 'rgb(123, 125, 127)'; - chart.data.datasets[0].borderWidth = 2.1; - - meta.controller.removeHoverStyle(point); - expect(point._model.backgroundColor).toBe('rgb(77, 79, 81)'); - expect(point._model.borderColor).toBe('rgb(123, 125, 127)'); - expect(point._model.borderWidth).toBe(2.1); - expect(point._model.radius).toBe(5); - - // Custom style - point.custom = { - radius: 4.4, - borderWidth: 5.5, - backgroundColor: 'rgb(0, 0, 0)', - borderColor: 'rgb(10, 10, 10)' - }; + it ('should handle hover styles defined via element options', function() { + var chart = this.chart; + var point = chart.getDatasetMeta(0).data[0]; + + Chart.helpers.merge(chart.options.elements.point, { + hoverBackgroundColor: 'rgb(200, 100, 150)', + hoverBorderColor: 'rgb(150, 50, 100)', + hoverBorderWidth: 8.4, + hoverRadius: 4.2 + }); + + chart.update(); + + triggerElementEvent(chart, 'mousemove', point); + expect(point._model.backgroundColor).toBe('rgb(200, 100, 150)'); + expect(point._model.borderColor).toBe('rgb(150, 50, 100)'); + expect(point._model.borderWidth).toBe(8.4); + expect(point._model.radius).toBe(20 + 4.2); + + triggerElementEvent(chart, 'mouseout', point); + expect(point._model.backgroundColor).toBe('rgb(100, 150, 200)'); + expect(point._model.borderColor).toBe('rgb(50, 100, 150)'); + expect(point._model.borderWidth).toBe(2); + expect(point._model.radius).toBe(20); + }); - meta.controller.removeHoverStyle(point); - expect(point._model.backgroundColor).toBe('rgb(0, 0, 0)'); - expect(point._model.borderColor).toBe('rgb(10, 10, 10)'); - expect(point._model.borderWidth).toBe(5.5); - expect(point._model.radius).toBe(4.4); + it ('should handle hover styles defined via element custom', function() { + var chart = this.chart; + var point = chart.getDatasetMeta(0).data[0]; + + point.custom = { + hoverBackgroundColor: 'rgb(200, 100, 150)', + hoverBorderColor: 'rgb(150, 50, 100)', + hoverBorderWidth: 8.4, + hoverRadius: 4.2 + }; + + chart.update(); + + triggerElementEvent(chart, 'mousemove', point); + expect(point._model.backgroundColor).toBe('rgb(200, 100, 150)'); + expect(point._model.borderColor).toBe('rgb(150, 50, 100)'); + expect(point._model.borderWidth).toBe(8.4); + expect(point._model.radius).toBe(20 + 4.2); + + triggerElementEvent(chart, 'mouseout', point); + expect(point._model.backgroundColor).toBe('rgb(100, 150, 200)'); + expect(point._model.borderColor).toBe('rgb(50, 100, 150)'); + expect(point._model.borderWidth).toBe(2); + expect(point._model.radius).toBe(20); + }); }); }); diff --git a/test/specs/helpers.options.tests.js b/test/specs/helpers.options.tests.js index 46e5222868f..3188888cb12 100644 --- a/test/specs/helpers.options.tests.js +++ b/test/specs/helpers.options.tests.js @@ -61,4 +61,65 @@ describe('Chart.helpers.options', function() { {top: 0, right: 0, bottom: 0, left: 0, height: 0, width: 0}); }); }); + + describe('resolve', function() { + it ('should fallback to the first defined input', function() { + expect(options.resolve([42])).toBe(42); + expect(options.resolve([42, 'foo'])).toBe(42); + expect(options.resolve([undefined, 42, 'foo'])).toBe(42); + expect(options.resolve([42, 'foo', undefined])).toBe(42); + expect(options.resolve([undefined])).toBe(undefined); + }); + it ('should correctly handle empty values (null, 0, "")', function() { + expect(options.resolve([0, 'foo'])).toBe(0); + expect(options.resolve(['', 'foo'])).toBe(''); + expect(options.resolve([null, 'foo'])).toBe(null); + }); + it ('should support indexable options if index is provided', function() { + var input = [42, 'foo', 'bar']; + expect(options.resolve([input], undefined, 0)).toBe(42); + expect(options.resolve([input], undefined, 1)).toBe('foo'); + expect(options.resolve([input], undefined, 2)).toBe('bar'); + }); + it ('should fallback if an indexable option value is undefined', function() { + var input = [42, undefined, 'bar']; + expect(options.resolve([input], undefined, 5)).toBe(undefined); + expect(options.resolve([input, 'foo'], undefined, 1)).toBe('foo'); + expect(options.resolve([input, 'foo'], undefined, 5)).toBe('foo'); + }); + it ('should not handle indexable options if index is undefined', function() { + var array = [42, 'foo', 'bar']; + expect(options.resolve([array])).toBe(array); + expect(options.resolve([array], undefined, undefined)).toBe(array); + }); + it ('should support scriptable options if context is provided', function() { + var input = function(context) { + return context.v * 2; + }; + expect(options.resolve([42], {v: 42})).toBe(42); + expect(options.resolve([input], {v: 42})).toBe(84); + }); + it ('should fallback if a scriptable option returns undefined', function() { + var input = function() {}; + expect(options.resolve([input], {v: 42})).toBe(undefined); + expect(options.resolve([input, 'foo'], {v: 42})).toBe('foo'); + expect(options.resolve([input, undefined, 'foo'], {v: 42})).toBe('foo'); + }); + it ('should not handle scriptable options if context is undefined', function() { + var input = function(context) { + return context.v * 2; + }; + expect(options.resolve([input])).toBe(input); + expect(options.resolve([input], undefined)).toBe(input); + }); + it ('should handle scriptable and indexable option', function() { + var input = function(context) { + return [context.v, undefined, 'bar']; + }; + expect(options.resolve([input, 'foo'], {v: 42}, 0)).toBe(42); + expect(options.resolve([input, 'foo'], {v: 42}, 1)).toBe('foo'); + expect(options.resolve([input, 'foo'], {v: 42}, 5)).toBe('foo'); + expect(options.resolve([input, ['foo', 'bar']], {v: 42}, 1)).toBe('bar'); + }); + }); }); From d1be09f2bbfde1a6bc8e06cedc72901ec16d3f7f Mon Sep 17 00:00:00 2001 From: Simon Brunel Date: Fri, 18 Aug 2017 10:03:45 +0200 Subject: [PATCH 2/3] Move test event helper under jasmine namespace --- test/jasmine.index.js | 1 + test/jasmine.utils.js | 15 ++++++++++++++ test/specs/controller.bubble.tests.js | 30 +++++++-------------------- 3 files changed, 24 insertions(+), 22 deletions(-) diff --git a/test/jasmine.index.js b/test/jasmine.index.js index 7e5fd67bee0..c1706130f2d 100644 --- a/test/jasmine.index.js +++ b/test/jasmine.index.js @@ -43,6 +43,7 @@ var utils = require('./jasmine.utils'); '}'); jasmine.specsFromFixtures = utils.specsFromFixtures; + jasmine.triggerMouseEvent = utils.triggerMouseEvent; beforeEach(function() { jasmine.addMatchers(matchers); diff --git a/test/jasmine.utils.js b/test/jasmine.utils.js index 473edfde9dc..c94249406a2 100644 --- a/test/jasmine.utils.js +++ b/test/jasmine.utils.js @@ -158,11 +158,26 @@ function waitForResize(chart, callback) { }; } +function triggerMouseEvent(chart, type, el) { + var node = chart.canvas; + var rect = node.getBoundingClientRect(); + var event = new MouseEvent(type, { + clientX: rect.left + el._model.x, + clientY: rect.top + el._model.y, + cancelable: true, + bubbles: true, + view: window + }); + + node.dispatchEvent(event); +} + module.exports = { injectCSS: injectCSS, createCanvas: createCanvas, acquireChart: acquireChart, releaseChart: releaseChart, specsFromFixtures: specsFromFixtures, + triggerMouseEvent: triggerMouseEvent, waitForResize: waitForResize }; diff --git a/test/specs/controller.bubble.tests.js b/test/specs/controller.bubble.tests.js index dce403d685a..3fa3eb883cd 100644 --- a/test/specs/controller.bubble.tests.js +++ b/test/specs/controller.bubble.tests.js @@ -274,20 +274,6 @@ describe('Bubble controller tests', function() { }); describe('Interactions', function() { - function triggerElementEvent(chart, type, el) { - var node = chart.canvas; - var rect = node.getBoundingClientRect(); - var event = new MouseEvent(type, { - clientX: rect.left + el._model.x, - clientY: rect.top + el._model.y, - cancelable: true, - bubbles: true, - view: window - }); - - node.dispatchEvent(event); - } - beforeEach(function() { this.chart = window.acquireChart({ type: 'bubble', @@ -330,13 +316,13 @@ describe('Bubble controller tests', function() { var chart = this.chart; var point = chart.getDatasetMeta(0).data[0]; - triggerElementEvent(chart, 'mousemove', point); + jasmine.triggerMouseEvent(chart, 'mousemove', point); expect(point._model.backgroundColor).toBe('rgb(49, 135, 221)'); expect(point._model.borderColor).toBe('rgb(22, 89, 156)'); expect(point._model.borderWidth).toBe(1); expect(point._model.radius).toBe(20 + 4); - triggerElementEvent(chart, 'mouseout', point); + jasmine.triggerMouseEvent(chart, 'mouseout', point); expect(point._model.backgroundColor).toBe('rgb(100, 150, 200)'); expect(point._model.borderColor).toBe('rgb(50, 100, 150)'); expect(point._model.borderWidth).toBe(2); @@ -356,13 +342,13 @@ describe('Bubble controller tests', function() { chart.update(); - triggerElementEvent(chart, 'mousemove', point); + jasmine.triggerMouseEvent(chart, 'mousemove', point); expect(point._model.backgroundColor).toBe('rgb(200, 100, 150)'); expect(point._model.borderColor).toBe('rgb(150, 50, 100)'); expect(point._model.borderWidth).toBe(8.4); expect(point._model.radius).toBe(20 + 4.2); - triggerElementEvent(chart, 'mouseout', point); + jasmine.triggerMouseEvent(chart, 'mouseout', point); expect(point._model.backgroundColor).toBe('rgb(100, 150, 200)'); expect(point._model.borderColor).toBe('rgb(50, 100, 150)'); expect(point._model.borderWidth).toBe(2); @@ -382,13 +368,13 @@ describe('Bubble controller tests', function() { chart.update(); - triggerElementEvent(chart, 'mousemove', point); + jasmine.triggerMouseEvent(chart, 'mousemove', point); expect(point._model.backgroundColor).toBe('rgb(200, 100, 150)'); expect(point._model.borderColor).toBe('rgb(150, 50, 100)'); expect(point._model.borderWidth).toBe(8.4); expect(point._model.radius).toBe(20 + 4.2); - triggerElementEvent(chart, 'mouseout', point); + jasmine.triggerMouseEvent(chart, 'mouseout', point); expect(point._model.backgroundColor).toBe('rgb(100, 150, 200)'); expect(point._model.borderColor).toBe('rgb(50, 100, 150)'); expect(point._model.borderWidth).toBe(2); @@ -408,13 +394,13 @@ describe('Bubble controller tests', function() { chart.update(); - triggerElementEvent(chart, 'mousemove', point); + jasmine.triggerMouseEvent(chart, 'mousemove', point); expect(point._model.backgroundColor).toBe('rgb(200, 100, 150)'); expect(point._model.borderColor).toBe('rgb(150, 50, 100)'); expect(point._model.borderWidth).toBe(8.4); expect(point._model.radius).toBe(20 + 4.2); - triggerElementEvent(chart, 'mouseout', point); + jasmine.triggerMouseEvent(chart, 'mouseout', point); expect(point._model.backgroundColor).toBe('rgb(100, 150, 200)'); expect(point._model.borderColor).toBe('rgb(50, 100, 150)'); expect(point._model.borderWidth).toBe(2); From a4564141fa89212b0dec17920f89dc745f8c3806 Mon Sep 17 00:00:00 2001 From: Simon Brunel Date: Sat, 19 Aug 2017 12:19:02 +0200 Subject: [PATCH 3/3] Minimal set of context properties --- docs/general/options.md | 9 +++------ samples/scriptable/bubble.html | 15 ++++++++------- src/controllers/controller.bubble.js | 7 ++----- 3 files changed, 13 insertions(+), 18 deletions(-) diff --git a/docs/general/options.md b/docs/general/options.md index f0b8746596b..2fd59029e64 100644 --- a/docs/general/options.md +++ b/docs/general/options.md @@ -37,12 +37,9 @@ The option context is used to give contextual information when resolving options The context object contains the following properties: -- `datasetIndex`: index of the current dataset -- `datasetCount`: number of datasets -- `dataset`: dataset at index `datasetIndex` -- `dataIndex`: index of the current data -- `dataCount`: number of data -- `data`: value of the current data - `chart`: the associated chart +- `dataIndex`: index of the current data +- `dataset`: dataset at index `datasetIndex` +- `datasetIndex`: index of the current dataset **Important**: since the context can represent different types of entities (dataset, data, etc.), some properties may be `undefined` so be sure to test any context property before using it. diff --git a/samples/scriptable/bubble.html b/samples/scriptable/bubble.html index 4f80c6089ed..feaf1ecb84c 100644 --- a/samples/scriptable/bubble.html +++ b/samples/scriptable/bubble.html @@ -30,13 +30,13 @@ utils.srand(110); function colorize(opaque, context) { - var data = context.data; - var x = data.x / 100; - var y = data.y / 100; + var value = context.dataset.data[context.dataIndex]; + var x = value.x / 100; + var y = value.y / 100; var r = x < 0 && y < 0 ? 250 : x < 0 ? 150 : y < 0 ? 50 : 0; var g = x < 0 && y < 0 ? 0 : x < 0 ? 50 : y < 0 ? 150 : 250; var b = x < 0 && y < 0 ? 0 : x > 0 && y > 0 ? 250 : 150; - var a = opaque ? 1 : 0.5 * data.v / 1000; + var a = opaque ? 1 : 0.5 * value.v / 1000; return 'rgba(' + r + ',' + g + ',' + b + ',' + a + ')'; } @@ -86,13 +86,14 @@ }, hoverBorderWidth: function(context) { - return Math.round(8 * context.data.v / 1000); + var value = context.dataset.data[context.dataIndex]; + return Math.round(8 * value.v / 1000); }, radius: function(context) { - var data = context.data; + var value = context.dataset.data[context.dataIndex]; var size = context.chart.width; - var base = Math.abs(data.v) / 1000; + var base = Math.abs(value.v) / 1000; return (size / 24) * base; } } diff --git a/src/controllers/controller.bubble.js b/src/controllers/controller.bubble.js index 2fbaf7401d4..65c6fae01b8 100644 --- a/src/controllers/controller.bubble.js +++ b/src/controllers/controller.bubble.js @@ -140,12 +140,9 @@ module.exports = function(Chart) { // Scriptable options var context = { chart: chart, - datasetIndex: me.index, - datasetCount: datasets.length, - dataset: dataset, dataIndex: index, - dataCount: dataset.data.length, - data: data + dataset: dataset, + datasetIndex: me.index }; var keys = [