diff --git a/docs/developers/axes.md b/docs/developers/axes.md index 682e93ea919..4b919815c96 100644 --- a/docs/developers/axes.md +++ b/docs/developers/axes.md @@ -73,8 +73,8 @@ To work with Chart.js, custom scale types must implement the following interface // buildTicks() should create a ticks array on the axis instance, if you intend to use any of the implementations from the base class buildTicks: function() {}, - // Get the value to show for the data at the given index of the the given dataset, ie this.chart.data.datasets[datasetIndex].data[index] - getLabelForIndex: function(index, datasetIndex) {}, + // Get the label to show for the given value + getLabelForValue: function(value) {}, // Get the pixel (x coordinate for horizontal axis, y coordinate for vertical axis) for a given value // @param index: index into the ticks array diff --git a/docs/getting-started/v3-migration.md b/docs/getting-started/v3-migration.md index 80f20139379..8587554cfa6 100644 --- a/docs/getting-started/v3-migration.md +++ b/docs/getting-started/v3-migration.md @@ -52,11 +52,13 @@ Chart.js is no longer providing the `Chart.bundle.js` and `Chart.bundle.min.js`. * `helpers.numberOfLabelLines` * `helpers.removeEvent` * `helpers.scaleMerge` +* `scale.getRightValue` * `scale.mergeTicksOptions` * `scale.ticksAsNumbers` * `Chart.Controller` * `Chart.chart.chart` * `Chart.types` +* `Line.calculatePointY` * Made `scale.handleDirectionalChanges` private * Made `scale.tickValues` private @@ -74,13 +76,18 @@ Chart.js is no longer providing the `Chart.bundle.js` and `Chart.bundle.min.js`. ### Changed -#### Ticks +#### Scales + +* `scale.getLabelForIndex` was replaced by `scale.getLabelForValue` +* `scale.getPixelForValue` now has only one parameter + +##### Ticks * `scale.ticks` now contains objects instead of strings * `buildTicks` is now expected to return tick objects * `afterBuildTicks` now has no parameters like the other callbacks * `convertTicksToLabels` was renamed to `generateTickLabels`. It is now expected to set the label property on the ticks given as input -#### Time Scale +##### Time Scale * `getValueForPixel` now returns milliseconds since the epoch diff --git a/src/controllers/controller.bar.js b/src/controllers/controller.bar.js index f7a4b221650..4bf0010eb7f 100644 --- a/src/controllers/controller.bar.js +++ b/src/controllers/controller.bar.js @@ -125,6 +125,57 @@ function computeFlexCategoryTraits(index, ruler, options) { }; } +function parseFloatBar(arr, item, vScale, i) { + var startValue = vScale._parse(arr[0], i); + var endValue = vScale._parse(arr[1], i); + var min = Math.min(startValue, endValue); + var max = Math.max(startValue, endValue); + var barStart = min; + var barEnd = max; + + if (Math.abs(min) > Math.abs(max)) { + barStart = max; + barEnd = min; + } + + // Store `barEnd` (furthest away from origin) as parsed value, + // to make stacking straight forward + item[vScale.id] = barEnd; + + item._custom = { + barStart: barStart, + barEnd: barEnd, + start: startValue, + end: endValue, + min: min, + max: max + }; +} + +function parseArrayOrPrimitive(meta, data, start, count) { + var iScale = this._getIndexScale(); + var vScale = this._getValueScale(); + var labels = iScale._getLabels(); + var singleScale = iScale === vScale; + var parsed = []; + var i, ilen, item, entry; + + for (i = start, ilen = start + count; i < ilen; ++i) { + entry = data[i]; + item = {}; + item[iScale.id] = singleScale || iScale._parse(labels[i], i); + + if (helpers.isArray(entry)) { + parseFloatBar(entry, item, vScale, i); + } else { + item[vScale.id] = vScale._parse(entry, i); + } + + parsed.push(item); + } + return parsed; +} + module.exports = DatasetController.extend({ dataElementType: elements.Rectangle, @@ -144,6 +195,24 @@ module.exports = DatasetController.extend({ 'minBarLength' ], + /** + * Overriding primitive data parsing since we support mixed primitive/array + * data for float bars + * @private + */ + _parsePrimitiveData: function() { + return parseArrayOrPrimitive.apply(this, arguments); + }, + + /** + * Overriding array data parsing since we support mixed primitive/array + * data for float bars + * @private + */ + _parseArrayData: function() { + return parseArrayOrPrimitive.apply(this, arguments); + }, + initialize: function() { var me = this; var meta; @@ -183,7 +252,8 @@ module.exports = DatasetController.extend({ label: me.chart.data.labels[index] }; - if (helpers.isArray(dataset.data[index])) { + // all borders are drawn for floating bar + if (me._getParsed(index)._custom) { rectangle._model.borderSkipped = null; } @@ -202,8 +272,8 @@ module.exports = DatasetController.extend({ var base = vscale.getBasePixel(); var horizontal = vscale.isHorizontal(); var ruler = me._ruler || me.getRuler(); - var vpixels = me.calculateBarValuePixels(me.index, index, options); - var ipixels = me.calculateBarIndexPixels(me.index, index, ruler, options); + var vpixels = me.calculateBarValuePixels(index, options); + var ipixels = me.calculateBarIndexPixels(index, ruler, options); model.horizontal = horizontal; model.base = reset ? base : vpixels.base; @@ -283,7 +353,7 @@ module.exports = DatasetController.extend({ var i, ilen; for (i = 0, ilen = me.getMeta().data.length; i < ilen; ++i) { - pixels.push(scale.getPixelForValue(null, i, me.index)); + pixels.push(scale.getPixelForValue(me._getParsed(i)[scale.id])); } return { @@ -299,52 +369,39 @@ module.exports = DatasetController.extend({ * Note: pixel values are not clamped to the scale area. * @private */ - calculateBarValuePixels: function(datasetIndex, index, options) { + calculateBarValuePixels: function(index, options) { var me = this; - var chart = me.chart; - var scale = me._getValueScale(); - var isHorizontal = scale.isHorizontal(); - var datasets = chart.data.datasets; - var metasets = scale._getMatchingVisibleMetas(me._type); - var value = scale._parseValue(datasets[datasetIndex].data[index]); + var valueScale = me._getValueScale(); var minBarLength = options.minBarLength; - var stacked = scale.options.stacked; - var stack = me.getMeta().stack; - var start = value.start === undefined ? 0 : value.max >= 0 && value.min >= 0 ? value.min : value.max; - var length = value.start === undefined ? value.end : value.max >= 0 && value.min >= 0 ? value.max - value.min : value.min - value.max; - var ilen = metasets.length; - var i, imeta, ivalue, base, head, size, stackLength; - - if (stacked || (stacked === undefined && stack !== undefined)) { - for (i = 0; i < ilen; ++i) { - imeta = metasets[i]; - - if (imeta.index === datasetIndex) { - break; - } - - if (imeta.stack === stack) { - stackLength = scale._parseValue(datasets[imeta.index].data[index]); - ivalue = stackLength.start === undefined ? stackLength.end : stackLength.min >= 0 && stackLength.max >= 0 ? stackLength.max : stackLength.min; + var start = 0; + var parsed = me._getParsed(index); + var value = parsed[valueScale.id]; + var custom = parsed._custom; + var length = me._cachedMeta._stacked ? me._applyStack(valueScale, parsed) : parsed[valueScale.id]; + var base, head, size; + + if (length !== value) { + start = length - value; + length = value; + } - if ((value.min < 0 && ivalue < 0) || (value.max >= 0 && ivalue > 0)) { - start += ivalue; - } - } + if (custom && custom.barStart !== undefined && custom.barEnd !== undefined) { + value = custom.barStart; + length = custom.barEnd - custom.barStart; + // bars crossing origin are not stacked + if (value !== 0 && Math.sign(value) !== Math.sign(custom.barEnd)) { + start = 0; } + start += value; } - base = scale.getPixelForValue(start); - head = scale.getPixelForValue(start + length); + base = valueScale.getPixelForValue(start); + head = valueScale.getPixelForValue(start + length); size = head - base; if (minBarLength !== undefined && Math.abs(size) < minBarLength) { - size = minBarLength; - if (length >= 0 && !isHorizontal || length < 0 && isHorizontal) { - head = base - minBarLength; - } else { - head = base + minBarLength; - } + size = size < 0 ? -minBarLength : minBarLength; + head = base + size; } return { @@ -358,13 +415,13 @@ module.exports = DatasetController.extend({ /** * @private */ - calculateBarIndexPixels: function(datasetIndex, index, ruler, options) { + calculateBarIndexPixels: function(index, ruler, options) { var me = this; var range = options.barThickness === 'flex' ? computeFlexCategoryTraits(index, ruler, options) : computeFitCategoryTraits(index, ruler, options); - var stackIndex = me.getStackIndex(datasetIndex, me.getMeta().stack); + var stackIndex = me.getStackIndex(me.index, me.getMeta().stack); var center = range.start + (range.chunk * stackIndex) + (range.chunk / 2); var size = Math.min( valueOrDefault(options.maxBarThickness, Infinity), @@ -383,15 +440,13 @@ module.exports = DatasetController.extend({ var chart = me.chart; var scale = me._getValueScale(); var rects = me.getMeta().data; - var dataset = me.getDataset(); var ilen = rects.length; var i = 0; helpers.canvas.clipArea(chart.ctx, chart.chartArea); for (; i < ilen; ++i) { - var val = scale._parseValue(dataset.data[i]); - if (!isNaN(val.min) && !isNaN(val.max)) { + if (!isNaN(me._getParsed(i)[scale.id])) { rects[i].draw(); } } diff --git a/src/controllers/controller.bubble.js b/src/controllers/controller.bubble.js index 87db319a89a..48c0a2ee46d 100644 --- a/src/controllers/controller.bubble.js +++ b/src/controllers/controller.bubble.js @@ -30,7 +30,7 @@ defaults._set('bubble', { }, label: function(item, data) { var datasetLabel = data.datasets[item.datasetIndex].label || ''; - var dataPoint = data.datasets[item.datasetIndex].data[item.index]; + var dataPoint = data.datasets[item.datasetIndex].data[item.index] || {r: '?'}; return datasetLabel + ': (' + item.label + ', ' + item.value + ', ' + dataPoint.r + ')'; } } @@ -59,6 +59,26 @@ module.exports = DatasetController.extend({ 'rotation' ], + /** + * Parse array of objects + * @private + */ + _parseObjectData: function(meta, data, start, count) { + var xScale = this.getScaleForId(meta.xAxisID); + var yScale = this.getScaleForId(meta.yAxisID); + var parsed = []; + var i, ilen, item, obj; + for (i = start, ilen = start + count; i < ilen; ++i) { + obj = data[i]; + item = {}; + item[xScale.id] = xScale._parseObject(obj, 'x', i); + item[yScale.id] = yScale._parseObject(obj, 'y', i); + item._custom = obj && obj.r && +obj.r; + parsed.push(item); + } + return parsed; + }, + /** * @protected */ @@ -82,14 +102,12 @@ module.exports = DatasetController.extend({ var xScale = me.getScaleForId(meta.xAxisID); var yScale = me.getScaleForId(meta.yAxisID); var options = me._resolveDataElementOptions(index); - var data = me.getDataset().data[index]; - var dsIndex = me.index; - - 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); + var parsed = !reset && me._getParsed(index); + var x = reset ? xScale.getPixelForDecimal(0.5) : xScale.getPixelForValue(parsed[xScale.id]); + var y = reset ? yScale.getBasePixel() : yScale.getPixelForValue(parsed[yScale.id]); point._options = options; - point._datasetIndex = dsIndex; + point._datasetIndex = me.index; point._index = index; point._model = { backgroundColor: options.backgroundColor, @@ -135,7 +153,7 @@ module.exports = DatasetController.extend({ var me = this; var chart = me.chart; var dataset = me.getDataset(); - var data = dataset.data[index] || {}; + var parsed = me._getParsed(index); var values = DatasetController.prototype._resolveDataElementOptions.apply(me, arguments); // Scriptable options @@ -153,7 +171,7 @@ module.exports = DatasetController.extend({ // Custom radius resolution values.radius = resolve([ - data.r, + parsed && parsed._custom, me._config.radius, chart.options.elements.point.radius ], context, index); diff --git a/src/controllers/controller.doughnut.js b/src/controllers/controller.doughnut.js index 7495d15faf6..099c88b9148 100644 --- a/src/controllers/controller.doughnut.js +++ b/src/controllers/controller.doughnut.js @@ -134,6 +134,19 @@ module.exports = DatasetController.extend({ 'hoverBorderWidth', ], + /** + * Override data parsing, since we are not using scales + * @private + */ + _parse: function(start, count) { + var data = this.getDataset().data; + var metaData = this.getMeta().data; + var i, ilen; + for (i = start, ilen = start + count; i < ilen; ++i) { + metaData[i]._val = +data[i]; + } + }, + // Get index of the dataset in relation to the visible datasets. This allows determining the inner and outer radius correctly getRingIndex: function(datasetIndex) { var ringIndex = 0; @@ -220,7 +233,7 @@ module.exports = DatasetController.extend({ var startAngle = opts.rotation; // non reset case handled later var endAngle = opts.rotation; // non reset case handled later var dataset = me.getDataset(); - var circumference = reset && animationOpts.animateRotate ? 0 : arc.hidden ? 0 : me.calculateCircumference(dataset.data[index]) * (opts.circumference / DOUBLE_PI); + var circumference = reset && animationOpts.animateRotate ? 0 : arc.hidden ? 0 : me.calculateCircumference(arc._val * opts.circumference / DOUBLE_PI); var innerRadius = reset && animationOpts.animateScale ? 0 : me.innerRadius; var outerRadius = reset && animationOpts.animateScale ? 0 : me.outerRadius; var options = arc._options || {}; @@ -264,14 +277,13 @@ module.exports = DatasetController.extend({ }, calculateTotal: function() { - var dataset = this.getDataset(); - var meta = this.getMeta(); + var metaData = this.getMeta().data; var total = 0; var value; - helpers.each(meta.data, function(element, index) { - value = dataset.data[index]; - if (!isNaN(value) && !element.hidden) { + helpers.each(metaData, function(arc) { + value = arc ? arc._val : NaN; + if (!isNaN(value) && !arc.hidden) { total += Math.abs(value); } }); diff --git a/src/controllers/controller.line.js b/src/controllers/controller.line.js index a8e242d3410..41066c0118a 100644 --- a/src/controllers/controller.line.js +++ b/src/controllers/controller.line.js @@ -115,16 +115,15 @@ module.exports = DatasetController.extend({ updateElement: function(point, index, reset) { var me = this; var meta = me.getMeta(); - var dataset = me.getDataset(); var datasetIndex = me.index; - var value = dataset.data[index]; + var xScale = me._xScale; + var yScale = me._yScale; var lineModel = meta.dataset._model; - var x, y; - + var stacked = meta._stacked; + var parsed = me._getParsed(index); var options = me._resolveDataElementOptions(index); - - x = me._xScale.getPixelForValue(typeof value === 'object' ? value : NaN, index, datasetIndex); - y = reset ? me._yScale.getBasePixel() : me.calculatePointY(value, index, datasetIndex); + var x = xScale.getPixelForValue(parsed[xScale.id]); + var y = reset ? yScale.getBasePixel() : yScale.getPixelForValue(stacked ? me._applyStack(yScale, parsed) : parsed[yScale.id]); // Utility point._options = options; @@ -170,43 +169,6 @@ module.exports = DatasetController.extend({ return values; }, - calculatePointY: function(value, index, datasetIndex) { - var me = this; - var chart = me.chart; - var yScale = me._yScale; - var sumPos = 0; - var sumNeg = 0; - var rightValue = +yScale.getRightValue(value); - var metasets = chart._getSortedVisibleDatasetMetas(); - var ilen = metasets.length; - var i, ds, dsMeta, stackedRightValue; - - if (yScale.options.stacked) { - for (i = 0; i < ilen; ++i) { - dsMeta = metasets[i]; - if (dsMeta.index === datasetIndex) { - break; - } - - ds = chart.data.datasets[dsMeta.index]; - if (dsMeta.type === 'line' && dsMeta.yAxisID === yScale.id) { - stackedRightValue = +yScale.getRightValue(ds.data[index]); - if (stackedRightValue < 0) { - sumNeg += stackedRightValue || 0; - } else { - sumPos += stackedRightValue || 0; - } - } - } - - if (rightValue < 0) { - return yScale.getPixelForValue(sumNeg + rightValue); - } - return yScale.getPixelForValue(sumPos + rightValue); - } - return yScale.getPixelForValue(value); - }, - updateBezierControlPoints: function() { var me = this; var chart = me.chart; diff --git a/src/controllers/controller.polarArea.js b/src/controllers/controller.polarArea.js index b73a938bb41..d2458bd7b2d 100644 --- a/src/controllers/controller.polarArea.js +++ b/src/controllers/controller.polarArea.js @@ -108,8 +108,6 @@ module.exports = DatasetController.extend({ dataElementType: elements.Arc, - linkScales: helpers.noop, - /** * @private */ diff --git a/src/controllers/controller.radar.js b/src/controllers/controller.radar.js index a690c75d05d..1a0bee2fada 100644 --- a/src/controllers/controller.radar.js +++ b/src/controllers/controller.radar.js @@ -24,8 +24,6 @@ module.exports = DatasetController.extend({ dataElementType: elements.Point, - linkScales: helpers.noop, - /** * @private */ diff --git a/src/core/core.datasetController.js b/src/core/core.datasetController.js index d885c53c536..a6e889d3e3d 100644 --- a/src/core/core.datasetController.js +++ b/src/core/core.datasetController.js @@ -75,6 +75,85 @@ function unlistenArrayEvents(array, listener) { delete array._chartjs; } +function getSortedDatasetIndices(chart, filterVisible) { + var keys = []; + var metasets = chart._getSortedDatasetMetas(filterVisible); + var i, ilen; + + for (i = 0, ilen = metasets.length; i < ilen; ++i) { + keys.push(metasets[i].index); + } + return keys; +} + +function applyStack(stack, value, dsIndex, allOther) { + var keys = stack.keys; + var i, ilen, datasetIndex, otherValue; + + for (i = 0, ilen = keys.length; i < ilen; ++i) { + datasetIndex = +keys[i]; + if (datasetIndex === dsIndex) { + if (allOther) { + continue; + } + break; + } + otherValue = stack.values[datasetIndex]; + if (!isNaN(otherValue) && (value === 0 || Math.sign(value) === Math.sign(otherValue))) { + value += otherValue; + } + } + return value; +} + +function convertObjectDataToArray(data) { + var keys = Object.keys(data); + var adata = []; + var i, ilen, key; + for (i = 0, ilen = keys.length; i < ilen; ++i) { + key = keys[i]; + adata.push({ + x: key, + y: data[key] + }); + } + return adata; +} + +function isStacked(scale, meta) { + var stacked = scale && scale.options.stacked; + return stacked || (stacked === undefined && meta.stack !== undefined); +} + +function getStackKey(xScale, yScale, meta) { + return isStacked(yScale, meta) && xScale.id + '.' + yScale.id + '.' + meta.stack + '.' + meta.type; +} + +function arraysEqual(array1, array2) { + var ilen = array1.length; + var i; + + if (ilen !== array2.length) { + return false; + } + + for (i = 0; i < ilen; i++) { + if (array1[i] !== array2[i]) { + return false; + } + } + return true; +} + +function getFirstScaleId(chart, axis) { + var scalesOpts = chart.options.scales; + var scale = chart.options.scale; + var scaleId = scale && scale.id; + var prop = axis + 'Axes'; + + return (scalesOpts && scalesOpts[prop] && scalesOpts[prop].length && scalesOpts[prop][0].id) || scaleId; +} + // Base class for all dataset controllers (line, bar, etc) var DatasetController = function(chart, datasetIndex) { this.initialize(chart, datasetIndex); @@ -125,11 +204,14 @@ helpers.extend(DatasetController.prototype, { initialize: function(chart, datasetIndex) { var me = this; + var meta; me.chart = chart; me.index = datasetIndex; + me._cachedMeta = meta = me.getMeta(); + me._type = meta.type; me.linkScales(); + meta._stacked = isStacked(me._getValueScale(), meta); me.addElements(); - me._type = me.getMeta().type; }, updateIndex: function(datasetIndex) { @@ -137,19 +219,12 @@ helpers.extend(DatasetController.prototype, { }, linkScales: function() { - var me = this; - var meta = me.getMeta(); - var chart = me.chart; - var scales = chart.scales; - var dataset = me.getDataset(); - var scalesOpts = chart.options.scales; + var chart = this.chart; + var meta = this._cachedMeta; + var dataset = this.getDataset(); - if (meta.xAxisID === null || !(meta.xAxisID in scales) || dataset.xAxisID) { - meta.xAxisID = dataset.xAxisID || scalesOpts.xAxes[0].id; - } - if (meta.yAxisID === null || !(meta.yAxisID in scales) || dataset.yAxisID) { - meta.yAxisID = dataset.yAxisID || scalesOpts.yAxes[0].id; - } + meta.xAxisID = dataset.xAxisID || getFirstScaleId(chart, 'x'); + meta.yAxisID = dataset.yAxisID || getFirstScaleId(chart, 'y'); }, getDataset: function() { @@ -168,14 +243,14 @@ helpers.extend(DatasetController.prototype, { * @private */ _getValueScaleId: function() { - return this.getMeta().yAxisID; + return this._cachedMeta.yAxisID; }, /** * @private */ _getIndexScaleId: function() { - return this.getMeta().xAxisID; + return this._cachedMeta.xAxisID; }, /** @@ -220,16 +295,76 @@ helpers.extend(DatasetController.prototype, { return type && new type({ _ctx: me.chart.ctx, _datasetIndex: me.index, - _index: index + _index: index, + _parsed: {} }); }, + /** + * @private + */ + _dataCheck: function() { + var me = this; + var dataset = me.getDataset(); + var data = dataset.data || (dataset.data = []); + + // In order to correctly handle data addition/deletion animation (an thus simulate + // real-time charts), we need to monitor these data modifications and synchronize + // the internal meta data accordingly. + + if (helpers.isObject(data)) { + // Object data is currently monitored for replacement only + if (me._objectData === data) { + return false; + } + me._data = convertObjectDataToArray(data); + me._objectData = data; + } else { + if (me._data === data && arraysEqual(data, me._dataCopy)) { + return false; + } + + if (me._data) { + // This case happens when the user replaced the data array instance. + unlistenArrayEvents(me._data, me); + } + + // Store a copy to detect direct modifications. + // Note: This is suboptimal, but better than always parsing the data + me._dataCopy = data.slice(0); + + if (data && Object.isExtensible(data)) { + listenArrayEvents(data, me); + } + me._data = data; + } + return true; + }, + + /** + * @private + */ + _labelCheck: function() { + var me = this; + var scale = me._getIndexScale(); + var labels = scale ? scale._getLabels() : me.chart.data.labels; + + if (me._labels === labels) { + return false; + } + + me._labels = labels; + return true; + }, + addElements: function() { var me = this; - var meta = me.getMeta(); - var data = me.getDataset().data || []; + var meta = me._cachedMeta; var metaData = meta.data; - var i, ilen; + var i, ilen, data; + + me._dataCheck(); + data = me._data; for (i = 0, ilen = data.length; i < ilen; ++i) { metaData[i] = metaData[i] || me.createMetaData(i); @@ -240,33 +375,23 @@ helpers.extend(DatasetController.prototype, { addElementAndReset: function(index) { var element = this.createMetaData(index); - this.getMeta().data.splice(index, 0, element); + this._cachedMeta.data.splice(index, 0, element); this.updateElement(element, index, true); }, buildOrUpdateElements: function() { var me = this; - var dataset = me.getDataset(); - var data = dataset.data || (dataset.data = []); + var dataChanged = me._dataCheck(); + var labelsChanged = me._labelCheck(); + var scaleChanged = me._scaleCheck(); + var meta = me._cachedMeta; - // In order to correctly handle data addition/deletion animation (an thus simulate - // real-time charts), we need to monitor these data modifications and synchronize - // the internal meta data accordingly. - if (me._data !== data) { - if (me._data) { - // This case happens when the user replaced the data array instance. - unlistenArrayEvents(me._data, me); - } - - if (data && Object.isExtensible(data)) { - listenArrayEvents(data, me); - } - me._data = data; - } + // make sure cached _stacked status is current + meta._stacked = isStacked(me._getValueScale(), meta); // Re-sync meta data in case the user replaced the data array or if we missed // any updates and so make sure that we handle number of datapoints changing. - me.resyncElements(); + me.resyncElements(dataChanged | labelsChanged | scaleChanged); }, /** @@ -287,17 +412,256 @@ helpers.extend(DatasetController.prototype, { }); }, + /** + * @private + */ + _parse: function(start, count) { + var me = this; + var chart = me.chart; + var meta = me._cachedMeta; + var data = me._data; + var crossRef = chart._xref || (chart._xref = {}); + var xScale = me._getIndexScale(); + var yScale = me._getValueScale(); + var xId = xScale.id; + var yId = yScale.id; + var xKey = getStackKey(xScale, yScale, meta); + var yKey = getStackKey(yScale, xScale, meta); + var stacks = xKey || yKey; + var i, ilen, parsed, stack, item, x, y; + + if (helpers.isArray(data[start])) { + parsed = me._parseArrayData(meta, data, start, count); + } else if (helpers.isObject(data[start])) { + parsed = me._parseObjectData(meta, data, start, count); + } else { + parsed = me._parsePrimitiveData(meta, data, start, count); + } + + function storeStack(stackKey, indexValue, scaleId, value) { + if (stackKey) { + stackKey += '.' + indexValue; + item._stackKeys[scaleId] = stackKey; + stack = crossRef[stackKey] || (crossRef[stackKey] = {}); + stack[meta.index] = value; + } + } + + for (i = 0, ilen = parsed.length; i < ilen; ++i) { + item = parsed[i]; + meta.data[start + i]._parsed = item; + + if (stacks) { + item._stackKeys = {}; + x = item[xId]; + y = item[yId]; + + storeStack(xKey, x, yId, y); + storeStack(yKey, y, xId, x); + } + } + + xScale._invalidateCaches(); + if (yScale !== xScale) { + yScale._invalidateCaches(); + } + }, + + /** + * Parse array of primitive values + * @param {object} meta - dataset meta + * @param {array} data - data array. Example [1,3,4] + * @param {number} start - start index + * @param {number} count - number of items to parse + * @returns {object} parsed item - item containing index and a parsed value + * for each scale id. + * Example: {xScale0: 0, yScale0: 1} + * @private + */ + _parsePrimitiveData: function(meta, data, start, count) { + var iScale = this._getIndexScale(); + var vScale = this._getValueScale(); + var labels = iScale._getLabels(); + var singleScale = iScale === vScale; + var parsed = []; + var i, ilen, item; + + for (i = start, ilen = start + count; i < ilen; ++i) { + item = {}; + item[iScale.id] = singleScale || iScale._parse(labels[i], i); + item[vScale.id] = vScale._parse(data[i], i); + parsed.push(item); + } + return parsed; + }, + + /** + * Parse array of arrays + * @param {object} meta - dataset meta + * @param {array} data - data array. Example [[1,2],[3,4]] + * @param {number} start - start index + * @param {number} count - number of items to parse + * @returns {object} parsed item - item containing index and a parsed value + * for each scale id. + * Example: {xScale0: 0, yScale0: 1} + * @private + */ + _parseArrayData: function(meta, data, start, count) { + var xScale = this.getScaleForId(meta.xAxisID); + var yScale = this.getScaleForId(meta.yAxisID); + var parsed = []; + var i, ilen, item, arr; + for (i = start, ilen = start + count; i < ilen; ++i) { + arr = data[i]; + item = {}; + item[xScale.id] = xScale._parse(arr[0], i); + item[yScale.id] = yScale._parse(arr[1], i); + parsed.push(item); + } + return parsed; + }, + + /** + * Parse array of objects + * @param {object} meta - dataset meta + * @param {array} data - data array. Example [{x:1, y:5}, {x:2, y:10}] + * @param {number} start - start index + * @param {number} count - number of items to parse + * @returns {object} parsed item - item containing index and a parsed value + * for each scale id. _custom is optional + * Example: {xScale0: 0, yScale0: 1, _custom: {r: 10, foo: 'bar'}} + * @private + */ + _parseObjectData: function(meta, data, start, count) { + var xScale = this.getScaleForId(meta.xAxisID); + var yScale = this.getScaleForId(meta.yAxisID); + var parsed = []; + var i, ilen, item, obj; + for (i = start, ilen = start + count; i < ilen; ++i) { + obj = data[i]; + item = {}; + item[xScale.id] = xScale._parseObject(obj, 'x', i); + item[yScale.id] = yScale._parseObject(obj, 'y', i); + parsed.push(item); + } + return parsed; + }, + + /** + * @private + */ + _getParsed: function(index) { + var data = this._cachedMeta.data; + if (index < 0 || index >= data.length) { + return; + } + return data[index]._parsed; + }, + + /** + * @private + */ + _applyStack: function(scale, parsed) { + var chart = this.chart; + var meta = this._cachedMeta; + var value = parsed[scale.id]; + var stack = { + keys: getSortedDatasetIndices(chart, true), + values: chart._xref[parsed._stackKeys[scale.id]] + }; + return applyStack(stack, value, meta.index); + }, + + _getMinMax: function(scale, canStack) { + var chart = this.chart; + var meta = this._cachedMeta; + var metaData = meta.data; + var ilen = metaData.length; + var crossRef = chart._xref || (chart._xref = {}); + var max = Number.NEGATIVE_INFINITY; + var stacked = canStack && meta._stacked; + var indices = getSortedDatasetIndices(chart, true); + var i, item, value, parsed, stack, min, minPositive; + + min = minPositive = Number.POSITIVE_INFINITY; + + for (i = 0; i < ilen; ++i) { + item = metaData[i]; + parsed = item._parsed; + value = parsed[scale.id]; + if (item.hidden || isNaN(value)) { + continue; + } + if (stacked) { + stack = { + keys: indices, + values: crossRef[parsed._stackKeys[scale.id]] + }; + value = applyStack(stack, value, meta.index, true); + } + min = Math.min(min, value); + max = Math.max(max, value); + if (value > 0) { + minPositive = Math.min(minPositive, value); + } + } + return { + min: min, + max: max, + minPositive: minPositive + }; + }, + + _getAllParsedValues: function(scale) { + var meta = this._cachedMeta; + var metaData = meta.data; + var values = []; + var i, ilen, value; + + for (i = 0, ilen = metaData.length; i < ilen; ++i) { + value = metaData[i]._parsed[scale.id]; + if (!isNaN(value)) { + values.push(value); + } + } + return values; + }, + + _cacheScaleStackStatus: function() { + var me = this; + var indexScale = me._getIndexScale(); + var valueScale = me._getValueScale(); + var cache = me._scaleStacked = {}; + if (indexScale && valueScale) { + cache[indexScale.id] = indexScale.options.stacked; + cache[valueScale.id] = valueScale.options.stacked; + } + }, + + _scaleCheck: function() { + var me = this; + var indexScale = me._getIndexScale(); + var valueScale = me._getValueScale(); + var cache = me._scaleStacked; + return !cache || + !indexScale || + !valueScale || + cache[indexScale.id] !== indexScale.options.stacked || + cache[valueScale.id] !== valueScale.options.stacked; + }, + _update: function(reset) { var me = this; me._configure(); me._cachedDataOpts = null; me.update(reset); + me._cacheScaleStackStatus(); }, update: helpers.noop, transition: function(easingValue) { - var meta = this.getMeta(); + var meta = this._cachedMeta; var elements = meta.data || []; var ilen = elements.length; var i = 0; @@ -312,7 +676,7 @@ helpers.extend(DatasetController.prototype, { }, draw: function() { - var meta = this.getMeta(); + var meta = this._cachedMeta; var elements = meta.data || []; var ilen = elements.length; var i = 0; @@ -334,7 +698,7 @@ helpers.extend(DatasetController.prototype, { */ getStyle: function(index) { var me = this; - var meta = me.getMeta(); + var meta = me._cachedMeta; var dataset = meta.dataset; var style; @@ -501,17 +865,19 @@ helpers.extend(DatasetController.prototype, { /** * @private */ - resyncElements: function() { + resyncElements: function(changed) { var me = this; - var meta = me.getMeta(); - var data = me.getDataset().data; + var meta = me._cachedMeta; var numMeta = meta.data.length; - var numData = data.length; + var numData = me._data.length; - if (numData < numMeta) { - meta.data.splice(numData, numMeta - numData); - } else if (numData > numMeta) { + if (numData > numMeta) { me.insertElements(numMeta, numData - numMeta); + } else if (numData < numMeta) { + meta.data.splice(numData, numMeta - numData); + me._parse(0, numData); + } else if (changed) { + me._parse(0, numData); } }, @@ -522,6 +888,7 @@ helpers.extend(DatasetController.prototype, { for (var i = 0; i < count; ++i) { this.addElementAndReset(start + i); } + this._parse(start, count); }, /** @@ -536,21 +903,21 @@ helpers.extend(DatasetController.prototype, { * @private */ onDataPop: function() { - this.getMeta().data.pop(); + this._cachedMeta.data.pop(); }, /** * @private */ onDataShift: function() { - this.getMeta().data.shift(); + this._cachedMeta.data.shift(); }, /** * @private */ onDataSplice: function(start, count) { - this.getMeta().data.splice(start, count); + this._cachedMeta.data.splice(start, count); this.insertElements(start, arguments.length - 2); }, diff --git a/src/core/core.scale.js b/src/core/core.scale.js index f3521ee1232..6cc8b3ab311 100644 --- a/src/core/core.scale.js +++ b/src/core/core.scale.js @@ -327,6 +327,56 @@ var Scale = Element.extend({ zeroLineIndex: 0, + /** + * Parse a supported input value to internal representation. + * @param {*} raw + * @param {number} index + * @private + * @since 3.0 + */ + _parse: function(raw, index) { // eslint-disable-line no-unused-vars + return raw; + }, + + /** + * Parse an object for axis to internal representation. + * @param {object} obj + * @param {string} axis + * @param {number} index + * @private + * @since 3.0 + */ + _parseObject: function(obj, axis, index) { + if (obj[axis] !== undefined) { + return this._parse(obj[axis], index); + } + return null; + }, + + _getMinMax: function(canStack) { + var me = this; + var metas = me._getMatchingVisibleMetas(); + var min = Number.POSITIVE_INFINITY; + var max = Number.NEGATIVE_INFINITY; + var minPositive = Number.POSITIVE_INFINITY; + var i, ilen, minmax; + + for (i = 0, ilen = metas.length; i < ilen; ++i) { + minmax = metas[i].controller._getMinMax(me, canStack); + min = Math.min(min, minmax.min); + max = Math.max(max, minmax.max); + minPositive = Math.min(minPositive, minmax.minPositive); + } + + return { + min: min, + max: max, + minPositive: minPositive + }; + }, + + _invalidateCaches: helpers.noop, + /** * Get the padding needed for the scale * @method getPadding @@ -734,32 +784,6 @@ var Scale = Element.extend({ return this.options.fullWidth; }, - // Get the correct value. NaN bad inputs, If the value type is object get the x or y based on whether we are horizontal or not - getRightValue: function(rawValue) { - // Null and undefined values first - if (isNullOrUndef(rawValue)) { - return NaN; - } - // isNaN(object) returns true, so make sure NaN is checking for a number; Discard Infinite values - if ((typeof rawValue === 'number' || rawValue instanceof Number) && !isFinite(rawValue)) { - return NaN; - } - - // If it is in fact an object, dive in one more level - if (rawValue) { - if (this.isHorizontal()) { - if (rawValue.x !== undefined) { - return this.getRightValue(rawValue.x); - } - } else if (rawValue.y !== undefined) { - return this.getRightValue(rawValue.y); - } - } - - // Value is good, return it - return rawValue; - }, - _convertTicksToLabels: function(ticks) { var me = this; @@ -786,51 +810,13 @@ var Scale = Element.extend({ }, /** - * @private + * Used to get the label to display in the tooltip for the given value + * @param value */ - _parseValue: function(value) { - var start, end, min, max; - - if (isArray(value)) { - start = +this.getRightValue(value[0]); - end = +this.getRightValue(value[1]); - min = Math.min(start, end); - max = Math.max(start, end); - } else { - value = +this.getRightValue(value); - start = undefined; - end = value; - min = value; - max = value; - } - - return { - min: min, - max: max, - start: start, - end: end - }; - }, - - /** - * @private - */ - _getScaleLabel: function(rawValue) { - var v = this._parseValue(rawValue); - if (v.start !== undefined) { - return '[' + v.start + ', ' + v.end + ']'; - } - - return +this.getRightValue(rawValue); + getLabelForValue: function(value) { + return value; }, - /** - * Used to get the value to display in the tooltip for the data at the given index - * @param index - * @param datasetIndex - */ - getLabelForIndex: helpers.noop, - /** * Returns the location of the given data point. Value can either be an index or a numerical value * The coordinate (0, 0) is at the upper-left corner of the canvas @@ -963,26 +949,13 @@ var Scale = Element.extend({ * @private */ _isVisible: function() { - var me = this; - var chart = me.chart; - var display = me.options.display; - var i, ilen, meta; + var display = this.options.display; if (display !== 'auto') { return !!display; } - // When 'auto', the scale is visible if at least one associated dataset is visible. - for (i = 0, ilen = chart.data.datasets.length; i < ilen; ++i) { - if (chart.isDatasetVisible(i)) { - meta = chart.getDatasetMeta(i); - if (meta.xAxisID === me.id || meta.yAxisID === me.id) { - return true; - } - } - } - - return false; + return this._getMatchingVisibleMetas().length > 0; }, /** @@ -1402,14 +1375,29 @@ var Scale = Element.extend({ /** * @private */ + _getAxisID: function() { + return this.isHorizontal() ? 'xAxisID' : 'yAxisID'; + }, + + /** + * Returns visible dataset metas that are attached to this scale + * @param {string} [type] - if specified, also filter by dataset type + * @private + */ _getMatchingVisibleMetas: function(type) { var me = this; - var isHorizontal = me.isHorizontal(); - return me.chart._getSortedVisibleDatasetMetas() - .filter(function(meta) { - return (!type || meta.type === type) - && (isHorizontal ? meta.xAxisID === me.id : meta.yAxisID === me.id); - }); + var metas = me.chart._getSortedVisibleDatasetMetas(); + var axisID = me._getAxisID(); + var result = []; + var i, ilen, meta; + + for (i = 0, ilen = metas.length; i < ilen; ++i) { + meta = metas[i]; + if (meta[axisID] === me.id && (!type || meta.type === type)) { + result.push(meta); + } + } + return result; } }); diff --git a/src/core/core.tooltip.js b/src/core/core.tooltip.js index f1aeee29894..38d4e990d7d 100644 --- a/src/core/core.tooltip.js +++ b/src/core/core.tooltip.js @@ -210,10 +210,11 @@ function createTooltipItem(chart, element) { var controller = chart.getDatasetMeta(datasetIndex).controller; var indexScale = controller._getIndexScale(); var valueScale = controller._getValueScale(); + var parsed = controller._getParsed(index); return { - label: indexScale ? '' + indexScale.getLabelForIndex(index, datasetIndex) : '', - value: valueScale ? '' + valueScale.getLabelForIndex(index, datasetIndex) : '', + label: indexScale ? '' + indexScale.getLabelForValue(parsed[indexScale.id]) : '', + value: valueScale ? '' + valueScale.getLabelForValue(parsed[valueScale.id]) : '', index: index, datasetIndex: datasetIndex, x: element._model.x, diff --git a/src/scales/scale.category.js b/src/scales/scale.category.js index 487f4b554ff..cb07a6751b2 100644 --- a/src/scales/scale.category.js +++ b/src/scales/scale.category.js @@ -1,15 +1,27 @@ 'use strict'; -var helpers = require('../helpers/index'); var Scale = require('../core/core.scale'); -var isNullOrUndef = helpers.isNullOrUndef; - var defaultConfig = { position: 'bottom' }; module.exports = Scale.extend({ + + _parse: function(raw, index) { + var labels = this._getLabels(); + var first = labels.indexOf(raw); + var last = labels.lastIndexOf(raw); + return first === -1 || first !== last ? index : first; + }, + + _parseObject: function(obj, axis, index) { + if (obj[axis] !== undefined) { + return this._parse(obj[axis], index); + } + return null; + }, + determineDataLimits: function() { var me = this; var labels = me._getLabels(); @@ -55,15 +67,14 @@ module.exports = Scale.extend({ }); }, - getLabelForIndex: function(index, datasetIndex) { + getLabelForValue: function(value) { var me = this; - var chart = me.chart; + var labels = me._getLabels(); - if (chart.getDatasetMeta(datasetIndex).controller._getValueScaleId() === me.id) { - return me.getRightValue(chart.data.datasets[datasetIndex].data[index]); + if (value >= 0 && value < labels.length) { + return labels[value]; } - - return me._getLabels()[index]; + return value; }, _configure: function() { @@ -87,36 +98,21 @@ module.exports = Scale.extend({ }, // Used to get data value locations. Value can either be an index or a numerical value - getPixelForValue: function(value, index, datasetIndex) { + getPixelForValue: function(value) { var me = this; - var valueCategory, labels, idx; - if (!isNullOrUndef(index) && !isNullOrUndef(datasetIndex)) { - value = me.chart.data.datasets[datasetIndex].data[index]; + if (typeof value !== 'number') { + value = me._parse(value); } - // If value is a data object, then index is the index in the data array, - // not the index of the scale. We need to change that. - if (!isNullOrUndef(value)) { - valueCategory = me.isHorizontal() ? value.x : value.y; - } - if (valueCategory !== undefined || (value !== undefined && isNaN(index))) { - labels = me._getLabels(); - value = helpers.valueOrDefault(valueCategory, value); - idx = labels.indexOf(value); - index = idx !== -1 ? idx : index; - if (isNaN(index)) { - index = value; - } - } - return me.getPixelForDecimal((index - me._startValue) / me._valueRange); + return me.getPixelForDecimal((value - me._startValue) / me._valueRange); }, getPixelForTick: function(index) { var ticks = this.ticks; return index < 0 || index > ticks.length - 1 ? null - : this.getPixelForValue(ticks[index], index + this.minIndex); + : this.getPixelForValue(index + this.minIndex); }, getValueForPixel: function(pixel) { diff --git a/src/scales/scale.linear.js b/src/scales/scale.linear.js index 21999c2be0b..84c3b19c5ea 100644 --- a/src/scales/scale.linear.js +++ b/src/scales/scale.linear.js @@ -11,108 +11,23 @@ var defaultConfig = { } }; -var DEFAULT_MIN = 0; -var DEFAULT_MAX = 1; - -function getOrCreateStack(stacks, stacked, meta) { - var key = [ - meta.type, - // we have a separate stack for stack=undefined datasets when the opts.stacked is undefined - stacked === undefined && meta.stack === undefined ? meta.index : '', - meta.stack - ].join('.'); - - if (stacks[key] === undefined) { - stacks[key] = { - pos: [], - neg: [] - }; - } - - return stacks[key]; -} - -function stackData(scale, stacks, meta, data) { - var opts = scale.options; - var stacked = opts.stacked; - var stack = getOrCreateStack(stacks, stacked, meta); - var pos = stack.pos; - var neg = stack.neg; - var ilen = data.length; - var i, value; - - for (i = 0; i < ilen; ++i) { - value = scale._parseValue(data[i]); - if (isNaN(value.min) || isNaN(value.max) || meta.data[i].hidden) { - continue; - } - - pos[i] = pos[i] || 0; - neg[i] = neg[i] || 0; - - if (value.min < 0 || value.max < 0) { - neg[i] += value.min; - } else { - pos[i] += value.max; - } - } -} - -function updateMinMax(scale, meta, data) { - var ilen = data.length; - var i, value; - - for (i = 0; i < ilen; ++i) { - value = scale._parseValue(data[i]); - if (isNaN(value.min) || isNaN(value.max) || meta.data[i].hidden) { - continue; - } - - scale.min = Math.min(scale.min, value.min); - scale.max = Math.max(scale.max, value.max); - } -} - module.exports = LinearScaleBase.extend({ determineDataLimits: function() { var me = this; - var opts = me.options; - var chart = me.chart; - var datasets = chart.data.datasets; - var metasets = me._getMatchingVisibleMetas(); - var hasStacks = opts.stacked; - var stacks = {}; - var ilen = metasets.length; - var i, meta, data, values; - - me.min = Number.POSITIVE_INFINITY; - me.max = Number.NEGATIVE_INFINITY; - - if (hasStacks === undefined) { - for (i = 0; !hasStacks && i < ilen; ++i) { - meta = metasets[i]; - hasStacks = meta.stack !== undefined; - } + var DEFAULT_MIN = 0; + var DEFAULT_MAX = 1; + var minmax = me._getMinMax(true); + var min = minmax.min; + var max = minmax.max; + + me.min = helpers.isFinite(min) && !isNaN(min) ? min : DEFAULT_MIN; + me.max = helpers.isFinite(max) && !isNaN(max) ? max : DEFAULT_MAX; + + // Backward compatible inconsistent min for stacked + if (me.options.stacked && min > 0) { + me.min = 0; } - for (i = 0; i < ilen; ++i) { - meta = metasets[i]; - data = datasets[meta.index].data; - if (hasStacks) { - stackData(me, stacks, meta, data); - } else { - updateMinMax(me, meta, data); - } - } - - helpers.each(stacks, function(stackValues) { - values = stackValues.pos.concat(stackValues.neg); - helpers._setMinAndMax(values, me); - }); - - me.min = helpers.isFinite(me.min) && !isNaN(me.min) ? me.min : DEFAULT_MIN; - me.max = helpers.isFinite(me.max) && !isNaN(me.max) ? me.max : DEFAULT_MAX; - // Common base implementation to handle ticks.min, ticks.max, ticks.beginAtZero me.handleTickRangeOptions(); }, @@ -138,14 +53,10 @@ module.exports = LinearScaleBase.extend({ return this.isHorizontal() ? ticks : ticks.reverse(); }, - getLabelForIndex: function(index, datasetIndex) { - return this._getScaleLabel(this.chart.data.datasets[datasetIndex].data[index]); - }, - // Utils getPixelForValue: function(value) { var me = this; - return me.getPixelForDecimal((+me.getRightValue(value) - me._startValue) / me._valueRange); + return me.getPixelForDecimal((value - me._startValue) / me._valueRange); }, getValueForPixel: function(pixel) { diff --git a/src/scales/scale.linearbase.js b/src/scales/scale.linearbase.js index b20820aaf5b..c420b601957 100644 --- a/src/scales/scale.linearbase.js +++ b/src/scales/scale.linearbase.js @@ -84,11 +84,15 @@ function generateTicks(generationOptions, dataRange) { } module.exports = Scale.extend({ - getRightValue: function(value) { - if (typeof value === 'string') { - return +value; + _parse: function(raw) { + if (helpers.isNullOrUndef(raw)) { + return NaN; } - return Scale.prototype.getRightValue.call(this, value); + if ((typeof raw === 'number' || raw instanceof Number) && !isFinite(raw)) { + return NaN; + } + + return +raw; }, handleTickRangeOptions: function() { diff --git a/src/scales/scale.logarithmic.js b/src/scales/scale.logarithmic.js index 43a9d46ab92..f85ed9549d6 100644 --- a/src/scales/scale.logarithmic.js +++ b/src/scales/scale.logarithmic.js @@ -3,6 +3,7 @@ var defaults = require('../core/core.defaults'); var helpers = require('../helpers/index'); var Scale = require('../core/core.scale'); +var LinearScaleBase = require('./scale.linearbase'); var Ticks = require('../core/core.ticks'); var valueOrDefault = helpers.valueOrDefault; @@ -69,100 +70,20 @@ function nonNegativeOrDefault(value, defaultValue) { } module.exports = Scale.extend({ + _parse: LinearScaleBase.prototype._parse, + determineDataLimits: function() { var me = this; - var opts = me.options; - var chart = me.chart; - var datasets = chart.data.datasets; - var isHorizontal = me.isHorizontal(); - function IDMatches(meta) { - return isHorizontal ? meta.xAxisID === me.id : meta.yAxisID === me.id; - } - var datasetIndex, meta, value, data, i, ilen; - - // Calculate Range - me.min = Number.POSITIVE_INFINITY; - me.max = Number.NEGATIVE_INFINITY; - me.minNotZero = Number.POSITIVE_INFINITY; - - var hasStacks = opts.stacked; - if (hasStacks === undefined) { - for (datasetIndex = 0; datasetIndex < datasets.length; datasetIndex++) { - meta = chart.getDatasetMeta(datasetIndex); - if (chart.isDatasetVisible(datasetIndex) && IDMatches(meta) && - meta.stack !== undefined) { - hasStacks = true; - break; - } - } - } - - if (opts.stacked || hasStacks) { - var valuesPerStack = {}; - - for (datasetIndex = 0; datasetIndex < datasets.length; datasetIndex++) { - meta = chart.getDatasetMeta(datasetIndex); - var key = [ - meta.type, - // we have a separate stack for stack=undefined datasets when the opts.stacked is undefined - ((opts.stacked === undefined && meta.stack === undefined) ? datasetIndex : ''), - meta.stack - ].join('.'); - - if (chart.isDatasetVisible(datasetIndex) && IDMatches(meta)) { - if (valuesPerStack[key] === undefined) { - valuesPerStack[key] = []; - } - - data = datasets[datasetIndex].data; - for (i = 0, ilen = data.length; i < ilen; i++) { - var values = valuesPerStack[key]; - value = me._parseValue(data[i]); - // invalid, hidden and negative values are ignored - if (isNaN(value.min) || isNaN(value.max) || meta.data[i].hidden || value.min < 0 || value.max < 0) { - continue; - } - values[i] = values[i] || 0; - values[i] += value.max; - } - } - } - - helpers.each(valuesPerStack, function(valuesForType) { - if (valuesForType.length > 0) { - helpers._setMinAndMax(valuesForType, me); - } - }); + var minmax = me._getMinMax(true); + var min = minmax.min; + var max = minmax.max; + var minPositive = minmax.minPositive; - } else { - for (datasetIndex = 0; datasetIndex < datasets.length; datasetIndex++) { - meta = chart.getDatasetMeta(datasetIndex); - if (chart.isDatasetVisible(datasetIndex) && IDMatches(meta)) { - data = datasets[datasetIndex].data; - for (i = 0, ilen = data.length; i < ilen; i++) { - value = me._parseValue(data[i]); - // invalid, hidden and negative values are ignored - if (isNaN(value.min) || isNaN(value.max) || meta.data[i].hidden || value.min < 0 || value.max < 0) { - continue; - } - - me.min = Math.min(value.min, me.min); - me.max = Math.max(value.max, me.max); - - if (value.min !== 0) { - me.minNotZero = Math.min(value.min, me.minNotZero); - } - } - } - } - } - - me.min = helpers.isFinite(me.min) ? me.min : null; - me.max = helpers.isFinite(me.max) ? me.max : null; - me.minNotZero = helpers.isFinite(me.minNotZero) ? me.minNotZero : null; + me.min = helpers.isFinite(min) ? Math.max(0, min) : null; + me.max = helpers.isFinite(max) ? Math.max(0, max) : null; + me.minNotZero = helpers.isFinite(minPositive) ? minPositive : null; - // Common base implementation to handle ticks.min, ticks.max - this.handleTickRangeOptions(); + me.handleTickRangeOptions(); }, handleTickRangeOptions: function() { @@ -237,11 +158,6 @@ module.exports = Scale.extend({ return Scale.prototype.generateTickLabels.call(this, ticks); }, - // Get the correct tooltip label - getLabelForIndex: function(index, datasetIndex) { - return this._getScaleLabel(this.chart.data.datasets[datasetIndex].data[index]); - }, - getPixelForTick: function(index) { var ticks = this._tickValues; if (index < 0 || index > ticks.length - 1) { @@ -284,8 +200,6 @@ module.exports = Scale.extend({ var me = this; var decimal = 0; - value = +me.getRightValue(value); - if (value > me.min && value > 0) { decimal = (log10(value) - me._startValue) / me._valueRange + me._valueOffset; } diff --git a/src/scales/scale.radialLinear.js b/src/scales/scale.radialLinear.js index 18216168da9..5ccf9a20d00 100644 --- a/src/scales/scale.radialLinear.js +++ b/src/scales/scale.radialLinear.js @@ -305,28 +305,12 @@ module.exports = LinearScaleBase.extend({ determineDataLimits: function() { var me = this; - var chart = me.chart; - var min = Number.POSITIVE_INFINITY; - var max = Number.NEGATIVE_INFINITY; - - helpers.each(chart.data.datasets, function(dataset, datasetIndex) { - if (chart.isDatasetVisible(datasetIndex)) { - var meta = chart.getDatasetMeta(datasetIndex); - - helpers.each(dataset.data, function(rawValue, index) { - var value = +me.getRightValue(rawValue); - if (isNaN(value) || meta.data[index].hidden) { - return; - } - - min = Math.min(value, min); - max = Math.max(value, max); - }); - } - }); + var minmax = me._getMinMax(false); + var min = minmax.min; + var max = minmax.max; - me.min = (min === Number.POSITIVE_INFINITY ? 0 : min); - me.max = (max === Number.NEGATIVE_INFINITY ? 0 : max); + me.min = helpers.isFinite(min) && !isNaN(min) ? min : 0; + me.max = helpers.isFinite(max) && !isNaN(max) ? max : 0; // Common base implementation to handle ticks.min, ticks.max, ticks.beginAtZero me.handleTickRangeOptions(); @@ -349,10 +333,6 @@ module.exports = LinearScaleBase.extend({ }); }, - getLabelForIndex: function(index, datasetIndex) { - return +this.getRightValue(this.chart.data.datasets[datasetIndex].data[index]); - }, - fit: function() { var me = this; var opts = me.options; diff --git a/src/scales/scale.time.js b/src/scales/scale.time.js index 453d18023d2..36024c1d164 100644 --- a/src/scales/scale.time.js +++ b/src/scales/scale.time.js @@ -9,7 +9,6 @@ var resolve = helpers.options.resolve; var valueOrDefault = helpers.valueOrDefault; // Integer constants are from the ES6 spec. -var MIN_INTEGER = Number.MIN_SAFE_INTEGER || -9007199254740991; var MAX_INTEGER = Number.MAX_SAFE_INTEGER || 9007199254740991; var INTERVALS = { @@ -204,7 +203,7 @@ function parse(scale, input) { } var options = scale.options.time; - var value = toTimestamp(scale, scale.getRightValue(input)); + var value = toTimestamp(scale, input); if (value === null) { return value; } @@ -365,6 +364,89 @@ function ticksFromTimestamps(scale, values, majorUnit) { return (ilen === 0 || !majorUnit) ? ticks : setMajorTicks(scale, ticks, map, majorUnit); } + +function getDataTimestamps(scale) { + var timestamps = scale._cache.data || []; + var i, ilen, metas; + + if (!timestamps.length) { + metas = scale._getMatchingVisibleMetas(); + for (i = 0, ilen = metas.length; i < ilen; ++i) { + timestamps = timestamps.concat(metas[i].controller._getAllParsedValues(scale)); + } + timestamps = scale._cache.data = arrayUnique(timestamps).sort(sorter); + } + return timestamps; +} + +function getLabelTimestamps(scale) { + var timestamps = scale._cache.labels || []; + var i, ilen, labels; + + if (!timestamps.length) { + labels = scale._getLabels(); + for (i = 0, ilen = labels.length; i < ilen; ++i) { + timestamps.push(parse(scale, labels[i])); + } + timestamps = scale._cache.labels = arrayUnique(timestamps).sort(sorter); + } + return timestamps; +} + +function getAllTimestamps(scale) { + var timestamps = scale._cache.all || []; + + if (!timestamps.length) { + timestamps = getDataTimestamps(scale).concat(getLabelTimestamps(scale)); + timestamps = scale._cache.all = arrayUnique(timestamps).sort(sorter); + } + return timestamps; +} + + +function getTimestampsForTicks(scale) { + var min = scale.min; + var max = scale.max; + var options = scale.options; + var capacity = scale.getLabelCapacity(min); + var source = options.ticks.source; + var timestamps; + + if (source === 'data' || (source === 'auto' && options.distribution === 'series')) { + timestamps = getAllTimestamps(scale); + } else if (source === 'labels') { + timestamps = getLabelTimestamps(scale); + } else { + timestamps = generate(scale, min, max, capacity, options); + } + + return timestamps; +} + +/** + * Return subset of `timestamps` between `min` and `max`. + * Timestamps are assumend to be in sorted order. + * @param {int[]} timestamps - array of timestamps + * @param {int} min - min value (timestamp) + * @param {int} max - max value (timestamp) + */ +function filterBetween(timestamps, min, max) { + var start = 0; + var end = timestamps.length - 1; + + while (start < end && timestamps[start] < min) { + start++; + } + while (end > start && timestamps[end] > max) { + end--; + } + end++; // slice does not include last element + + return start > 0 || end < timestamps.length + ? timestamps.slice(start, end) + : timestamps; +} + var defaultConfig = { position: 'bottom', @@ -415,158 +497,107 @@ var defaultConfig = { }; module.exports = Scale.extend({ + _parse: function(raw, index) { // eslint-disable-line no-unused-vars + if (raw === undefined) { + return NaN; + } + return toTimestamp(this, raw); + }, + + _parseObject: function(obj, axis, index) { + if (obj && obj.t) { + return this._parse(obj.t, index); + } + if (obj[axis] !== undefined) { + return this._parse(obj[axis], index); + } + return null; + }, - update: function() { + _invalidateCaches: function() { + this._cache = {}; + }, + + initialize: function() { var me = this; var options = me.options; var time = options.time || (options.time = {}); var adapter = me._adapter = new adapters._date(options.adapters.date); + + me._cache = {}; + // Backward compatibility: before introducing adapter, `displayFormats` was // supposed to contain *all* unit/string pairs but this can't be resolved // when loading the scale (adapters are loaded afterward), so let's populate // missing formats on update - helpers.mergeIf(time.displayFormats, adapter.formats()); - return Scale.prototype.update.apply(me, arguments); - }, + helpers.mergeIf(time.displayFormats, adapter.formats()); - /** - * Allows data to be referenced via 't' attribute - */ - getRightValue: function(rawValue) { - if (rawValue && rawValue.t !== undefined) { - rawValue = rawValue.t; - } - return Scale.prototype.getRightValue.call(this, rawValue); + Scale.prototype.initialize.call(me); }, determineDataLimits: function() { var me = this; - var chart = me.chart; var adapter = me._adapter; var options = me.options; var tickOpts = options.ticks; var unit = options.time.unit || 'day'; - var min = MAX_INTEGER; - var max = MIN_INTEGER; - var timestamps = []; - var datasets = []; - var labels = []; - var i, j, ilen, jlen, data, timestamp, labelsAdded; - var dataLabels = me._getLabels(); - - for (i = 0, ilen = dataLabels.length; i < ilen; ++i) { - labels.push(parse(me, dataLabels[i])); + var min = Number.POSITIVE_INFINITY; + var max = Number.NEGATIVE_INFINITY; + var minmax = me._getMinMax(false); + var i, ilen, labels; + + min = Math.min(min, minmax.min); + max = Math.max(max, minmax.max); + + labels = getLabelTimestamps(me); + for (i = 0, ilen = labels.length; i < ilen; ++i) { + min = Math.min(min, labels[i]); + max = Math.max(max, labels[i]); } - for (i = 0, ilen = (chart.data.datasets || []).length; i < ilen; ++i) { - if (chart.isDatasetVisible(i)) { - data = chart.data.datasets[i].data; - - // Let's consider that all data have the same format. - if (helpers.isObject(data[0])) { - datasets[i] = []; - - for (j = 0, jlen = data.length; j < jlen; ++j) { - timestamp = parse(me, data[j]); - timestamps.push(timestamp); - datasets[i][j] = timestamp; - } - } else { - datasets[i] = labels.slice(0); - if (!labelsAdded) { - timestamps = timestamps.concat(labels); - labelsAdded = true; - } - } - } else { - datasets[i] = []; - } - } - - if (labels.length) { - min = Math.min(min, labels[0]); - max = Math.max(max, labels[labels.length - 1]); - } - - if (timestamps.length) { - timestamps = ilen > 1 ? arrayUnique(timestamps).sort(sorter) : timestamps.sort(sorter); - min = Math.min(min, timestamps[0]); - max = Math.max(max, timestamps[timestamps.length - 1]); - } + min = helpers.isFinite(min) && !isNaN(min) ? min : +adapter.startOf(Date.now(), unit); + max = helpers.isFinite(max) && !isNaN(max) ? max : +adapter.endOf(Date.now(), unit) + 1; min = parse(me, tickOpts.min) || min; max = parse(me, tickOpts.max) || max; - // In case there is no valid min/max, set limits based on unit time option - min = min === MAX_INTEGER ? +adapter.startOf(Date.now(), unit) : min; - max = max === MIN_INTEGER ? +adapter.endOf(Date.now(), unit) + 1 : max; - // Make sure that max is strictly higher than min (required by the lookup table) me.min = Math.min(min, max); me.max = Math.max(min + 1, max); - - // PRIVATE - me._table = []; - me._timestamps = { - data: timestamps, - datasets: datasets, - labels: labels - }; }, buildTicks: function() { var me = this; - var min = me.min; - var max = me.max; var options = me.options; - var tickOpts = options.ticks; var timeOpts = options.time; - var timestamps = me._timestamps; - var ticks = []; - var capacity = me.getLabelCapacity(min); - var source = tickOpts.source; + var tickOpts = options.ticks; var distribution = options.distribution; - var i, ilen, timestamp; + var ticks = []; + var min, max, timestamps; - if (source === 'data' || (source === 'auto' && distribution === 'series')) { - timestamps = timestamps.data; - } else if (source === 'labels') { - timestamps = timestamps.labels; - } else { - timestamps = generate(me, min, max, capacity, options); - } + timestamps = getTimestampsForTicks(me); if (options.bounds === 'ticks' && timestamps.length) { - min = timestamps[0]; - max = timestamps[timestamps.length - 1]; + me.min = parse(me, tickOpts.min) || timestamps[0]; + me.max = parse(me, tickOpts.max) || timestamps[timestamps.length - 1]; } - // Enforce limits with user min/max options - min = parse(me, tickOpts.min) || min; - max = parse(me, tickOpts.max) || max; - - // Remove ticks outside the min/max range - for (i = 0, ilen = timestamps.length; i < ilen; ++i) { - timestamp = timestamps[i]; - if (timestamp >= min && timestamp <= max) { - ticks.push(timestamp); - } - } + min = me.min; + max = me.max; - me.min = min; - me.max = max; + ticks = filterBetween(timestamps, min, max); // PRIVATE // determineUnitForFormatting relies on the number of ticks so we don't use it when // autoSkip is enabled because we don't yet know what the final number of ticks will be me._unit = timeOpts.unit || (tickOpts.autoSkip - ? determineUnitForAutoTicks(timeOpts.minUnit, me.min, me.max, capacity) + ? determineUnitForAutoTicks(timeOpts.minUnit, me.min, me.max, me.getLabelCapacity(min)) : determineUnitForFormatting(me, ticks.length, timeOpts.minUnit, me.min, me.max)); me._majorUnit = !tickOpts.major.enabled || me._unit === 'year' ? undefined : determineMajorUnit(me._unit); - me._table = buildLookupTable(me._timestamps.data, min, max, distribution); + me._table = buildLookupTable(getAllTimestamps(me), min, max, distribution); me._offsets = computeOffsets(me._table, ticks, min, max, options); if (tickOpts.reverse) { @@ -576,24 +607,15 @@ module.exports = Scale.extend({ return ticksFromTimestamps(me, ticks, me._majorUnit); }, - getLabelForIndex: function(index, datasetIndex) { + getLabelForValue: function(value) { var me = this; var adapter = me._adapter; - var data = me.chart.data; var timeOpts = me.options.time; - var label = data.labels && index < data.labels.length ? data.labels[index] : ''; - var value = data.datasets[datasetIndex].data[index]; - if (helpers.isObject(value)) { - label = me.getRightValue(value); - } if (timeOpts.tooltipFormat) { - return adapter.format(toTimestamp(me, label), timeOpts.tooltipFormat); - } - if (typeof label === 'string') { - return label; + return adapter.format(value, timeOpts.tooltipFormat); } - return adapter.format(toTimestamp(me, label), timeOpts.displayFormats.datetime); + return adapter.format(value, timeOpts.displayFormats.datetime); }, /** @@ -640,20 +662,15 @@ module.exports = Scale.extend({ return me.getPixelForDecimal((offsets.start + pos) * offsets.factor); }, - getPixelForValue: function(value, index, datasetIndex) { + getPixelForValue: function(value) { var me = this; - var time = null; - - if (index !== undefined && datasetIndex !== undefined) { - time = me._timestamps.datasets[datasetIndex][index]; - } - if (time === null) { - time = parse(me, value); + if (typeof value !== 'number') { + value = parse(me, value); } - if (time !== null) { - return me.getPixelForOffset(time); + if (value !== null) { + return me.getPixelForOffset(value); } }, diff --git a/test/fixtures/controller.bar/data/object.js b/test/fixtures/controller.bar/data/object.js new file mode 100644 index 00000000000..f5b6ac2256b --- /dev/null +++ b/test/fixtures/controller.bar/data/object.js @@ -0,0 +1,32 @@ +module.exports = { + config: { + type: 'bar', + data: { + labels: ['a', 'b', 'c'], + datasets: [ + { + data: {a: 10, b: 2, c: -5}, + backgroundColor: '#ff0000' + }, + { + data: {a: 8, b: 12, c: 5}, + backgroundColor: '#00ff00' + } + ] + }, + options: { + legend: false, + title: false, + scales: { + xAxes: [{display: false}], + yAxes: [{display: false}] + } + } + }, + options: { + canvas: { + height: 256, + width: 512 + } + } +}; diff --git a/test/fixtures/controller.bar/data/object.png b/test/fixtures/controller.bar/data/object.png new file mode 100644 index 00000000000..c705448f0d1 Binary files /dev/null and b/test/fixtures/controller.bar/data/object.png differ diff --git a/test/fixtures/controller.bar/floatBar/float-bar-stacked-horizontal.png b/test/fixtures/controller.bar/floatBar/float-bar-stacked-horizontal.png index 5df57310985..c3066fd6812 100644 Binary files a/test/fixtures/controller.bar/floatBar/float-bar-stacked-horizontal.png and b/test/fixtures/controller.bar/floatBar/float-bar-stacked-horizontal.png differ diff --git a/test/fixtures/controller.bar/floatBar/float-bar-stacked.png b/test/fixtures/controller.bar/floatBar/float-bar-stacked.png index c9c02ea2f7b..5da257bcd43 100644 Binary files a/test/fixtures/controller.bar/floatBar/float-bar-stacked.png and b/test/fixtures/controller.bar/floatBar/float-bar-stacked.png differ diff --git a/test/fixtures/controller.bubble/radius-data.js b/test/fixtures/controller.bubble/radius-data.js new file mode 100644 index 00000000000..0c90917fdc3 --- /dev/null +++ b/test/fixtures/controller.bubble/radius-data.js @@ -0,0 +1,47 @@ +module.exports = { + config: { + type: 'bubble', + data: { + datasets: [{ + data: [ + {x: 0, y: 5, r: 1}, + {x: 1, y: 4, r: 2}, + {x: 2, y: 3, r: 6}, + {x: 3, y: 2}, + {x: 4, y: 1, r: 2}, + {x: 5, y: 0, r: NaN}, + {x: 6, y: -1, r: undefined}, + {x: 7, y: -2, r: null}, + {x: 8, y: -3, r: '4'}, + {x: 9, y: -4, r: '4px'}, + ] + }] + }, + options: { + legend: false, + title: false, + scales: { + xAxes: [{display: false}], + yAxes: [{display: false}] + }, + elements: { + point: { + backgroundColor: '#444', + radius: 10 + } + }, + layout: { + padding: { + left: 24, + right: 24 + } + } + } + }, + options: { + canvas: { + height: 128, + width: 256 + } + } +}; diff --git a/test/fixtures/controller.bubble/radius-data.png b/test/fixtures/controller.bubble/radius-data.png new file mode 100644 index 00000000000..ac819c21e45 Binary files /dev/null and b/test/fixtures/controller.bubble/radius-data.png differ diff --git a/test/specs/scale.category.tests.js b/test/specs/scale.category.tests.js index 33727960859..c8b5957cd31 100644 --- a/test/specs/scale.category.tests.js +++ b/test/specs/scale.category.tests.js @@ -192,7 +192,7 @@ describe('Category scale tests', function() { var scale = chart.scales.xScale0; - expect(scale.getLabelForIndex(1, 0)).toBe('tick2'); + expect(scale.getLabelForValue(1)).toBe('tick2'); }); it('Should get the correct pixel for a value when horizontal', function() { @@ -222,19 +222,19 @@ describe('Category scale tests', function() { }); var xScale = chart.scales.xScale0; - expect(xScale.getPixelForValue(0, 0, 0)).toBeCloseToPixel(23 + 6); // plus lineHeight + expect(xScale.getPixelForValue(0)).toBeCloseToPixel(23 + 6); // plus lineHeight expect(xScale.getValueForPixel(23)).toBe(0); - expect(xScale.getPixelForValue(0, 4, 0)).toBeCloseToPixel(487); + expect(xScale.getPixelForValue(4)).toBeCloseToPixel(487); expect(xScale.getValueForPixel(487)).toBe(4); xScale.options.offset = true; chart.update(); - expect(xScale.getPixelForValue(0, 0, 0)).toBeCloseToPixel(71 + 6); // plus lineHeight + expect(xScale.getPixelForValue(0)).toBeCloseToPixel(71 + 6); // plus lineHeight expect(xScale.getValueForPixel(69)).toBe(0); - expect(xScale.getPixelForValue(0, 4, 0)).toBeCloseToPixel(461); + expect(xScale.getPixelForValue(4)).toBeCloseToPixel(461); expect(xScale.getValueForPixel(417)).toBe(4); }); @@ -265,8 +265,7 @@ describe('Category scale tests', function() { }); var xScale = chart.scales.xScale0; - expect(xScale.getPixelForValue('tick_1', 0, 0)).toBeCloseToPixel(23 + 6); // plus lineHeight - expect(xScale.getPixelForValue('tick_1', 1, 0)).toBeCloseToPixel(143); + expect(xScale.getPixelForValue('tick1')).toBeCloseToPixel(23 + 6); // plus lineHeight }); it('Should get the correct pixel for a value when horizontal and zoomed', function() { @@ -300,14 +299,14 @@ describe('Category scale tests', function() { }); var xScale = chart.scales.xScale0; - expect(xScale.getPixelForValue(0, 1, 0)).toBeCloseToPixel(23 + 6); // plus lineHeight - expect(xScale.getPixelForValue(0, 3, 0)).toBeCloseToPixel(496); + expect(xScale.getPixelForValue(1)).toBeCloseToPixel(23 + 6); // plus lineHeight + expect(xScale.getPixelForValue(3)).toBeCloseToPixel(496); xScale.options.offset = true; chart.update(); - expect(xScale.getPixelForValue(0, 1, 0)).toBeCloseToPixel(103 + 6); // plus lineHeight - expect(xScale.getPixelForValue(0, 3, 0)).toBeCloseToPixel(429); + expect(xScale.getPixelForValue(1)).toBeCloseToPixel(103 + 6); // plus lineHeight + expect(xScale.getPixelForValue(3)).toBeCloseToPixel(429); }); it('should get the correct pixel for a value when vertical', function() { @@ -339,20 +338,20 @@ describe('Category scale tests', function() { }); var yScale = chart.scales.yScale0; - expect(yScale.getPixelForValue(0, 0, 0)).toBeCloseToPixel(32); - expect(yScale.getValueForPixel(32)).toBe(0); + expect(yScale.getPixelForValue(0)).toBeCloseToPixel(32); + expect(yScale.getValueForPixel(257)).toBe(2); - expect(yScale.getPixelForValue(0, 4, 0)).toBeCloseToPixel(484); - expect(yScale.getValueForPixel(484)).toBe(4); + expect(yScale.getPixelForValue(4)).toBeCloseToPixel(481); + expect(yScale.getValueForPixel(144)).toBe(1); yScale.options.offset = true; chart.update(); - expect(yScale.getPixelForValue(0, 0, 0)).toBeCloseToPixel(77); - expect(yScale.getValueForPixel(77)).toBe(0); + expect(yScale.getPixelForValue(0)).toBeCloseToPixel(77); + expect(yScale.getValueForPixel(256)).toBe(2); - expect(yScale.getPixelForValue(0, 4, 0)).toBeCloseToPixel(437); - expect(yScale.getValueForPixel(437)).toBe(4); + expect(yScale.getPixelForValue(4)).toBeCloseToPixel(436); + expect(yScale.getValueForPixel(167)).toBe(1); }); it('should get the correct pixel for a value when vertical and zoomed', function() { @@ -389,14 +388,14 @@ describe('Category scale tests', function() { var yScale = chart.scales.yScale0; - expect(yScale.getPixelForValue(0, 1, 0)).toBeCloseToPixel(32); - expect(yScale.getPixelForValue(0, 3, 0)).toBeCloseToPixel(484); + expect(yScale.getPixelForValue(1)).toBeCloseToPixel(32); + expect(yScale.getPixelForValue(3)).toBeCloseToPixel(482); yScale.options.offset = true; chart.update(); - expect(yScale.getPixelForValue(0, 1, 0)).toBeCloseToPixel(107); - expect(yScale.getPixelForValue(0, 3, 0)).toBeCloseToPixel(407); + expect(yScale.getPixelForValue(1)).toBeCloseToPixel(107); + expect(yScale.getPixelForValue(3)).toBeCloseToPixel(407); }); it('Should get the correct pixel for an object value when horizontal', function() { @@ -432,9 +431,9 @@ describe('Category scale tests', function() { }); var xScale = chart.scales.xScale0; - expect(xScale.getPixelForValue({x: 0, y: 10}, 0, 0)).toBeCloseToPixel(29); - expect(xScale.getPixelForValue({x: 3, y: 25}, 3, 0)).toBeCloseToPixel(506); - expect(xScale.getPixelForValue({x: 0, y: 78}, 4, 0)).toBeCloseToPixel(29); + expect(xScale.getPixelForValue(0)).toBeCloseToPixel(29); + expect(xScale.getPixelForValue(3)).toBeCloseToPixel(506); + expect(xScale.getPixelForValue(4)).toBeCloseToPixel(664); }); it('Should get the correct pixel for an object value when vertical', function() { @@ -472,8 +471,8 @@ describe('Category scale tests', function() { }); var yScale = chart.scales.yScale0; - expect(yScale.getPixelForValue({x: 0, y: 2}, 0, 0)).toBeCloseToPixel(257); - expect(yScale.getPixelForValue({x: 0, y: 1}, 4, 0)).toBeCloseToPixel(144); + expect(yScale.getPixelForValue(0)).toBeCloseToPixel(32); + expect(yScale.getPixelForValue(4)).toBeCloseToPixel(481); }); it('Should get the correct pixel for an object value in a bar chart', function() { @@ -509,9 +508,9 @@ describe('Category scale tests', function() { }); var xScale = chart.scales.xScale0; - expect(xScale.getPixelForValue(null, 0, 0)).toBeCloseToPixel(89); - expect(xScale.getPixelForValue(null, 3, 0)).toBeCloseToPixel(449); - expect(xScale.getPixelForValue(null, 4, 0)).toBeCloseToPixel(89); + expect(xScale.getPixelForValue(0)).toBeCloseToPixel(89); + expect(xScale.getPixelForValue(3)).toBeCloseToPixel(449); + expect(xScale.getPixelForValue(4)).toBeCloseToPixel(569); }); it('Should get the correct pixel for an object value in a horizontal bar chart', function() { @@ -547,8 +546,8 @@ describe('Category scale tests', function() { }); var yScale = chart.scales.yScale0; - expect(yScale.getPixelForValue(null, 0, 0)).toBeCloseToPixel(88); - expect(yScale.getPixelForValue(null, 3, 0)).toBeCloseToPixel(426); - expect(yScale.getPixelForValue(null, 4, 0)).toBeCloseToPixel(88); + expect(yScale.getPixelForValue(0)).toBeCloseToPixel(88); + expect(yScale.getPixelForValue(3)).toBeCloseToPixel(426); + expect(yScale.getPixelForValue(4)).toBeCloseToPixel(538); }); }); diff --git a/test/specs/scale.linear.tests.js b/test/specs/scale.linear.tests.js index 79d7fcd7f70..2c185178360 100644 --- a/test/specs/scale.linear.tests.js +++ b/test/specs/scale.linear.tests.js @@ -323,7 +323,7 @@ describe('Linear Scale', function() { }); chart.update(); - expect(chart.scales.yScale0.getLabelForIndex(3, 0)).toBe(7); + expect(chart.scales.yScale0.getLabelForValue(7)).toBe(7); }); it('Should correctly determine the min and max data values when stacked mode is turned on', function() { @@ -857,18 +857,18 @@ describe('Linear Scale', function() { }); var xScale = chart.scales.xScale0; - expect(xScale.getPixelForValue(1, 0, 0)).toBeCloseToPixel(501); // right - paddingRight - expect(xScale.getPixelForValue(-1, 0, 0)).toBeCloseToPixel(31 + 6); // left + paddingLeft + lineSpace - expect(xScale.getPixelForValue(0, 0, 0)).toBeCloseToPixel(266 + 6 / 2); // halfway*/ + expect(xScale.getPixelForValue(1)).toBeCloseToPixel(501); // right - paddingRight + expect(xScale.getPixelForValue(-1)).toBeCloseToPixel(31 + 6); // left + paddingLeft + lineSpace + expect(xScale.getPixelForValue(0)).toBeCloseToPixel(266 + 6 / 2); // halfway*/ expect(xScale.getValueForPixel(501)).toBeCloseTo(1, 1e-2); expect(xScale.getValueForPixel(31)).toBeCloseTo(-1, 1e-2); expect(xScale.getValueForPixel(266)).toBeCloseTo(0, 1e-2); var yScale = chart.scales.yScale0; - expect(yScale.getPixelForValue(1, 0, 0)).toBeCloseToPixel(32); // right - paddingRight - expect(yScale.getPixelForValue(-1, 0, 0)).toBeCloseToPixel(484); // left + paddingLeft - expect(yScale.getPixelForValue(0, 0, 0)).toBeCloseToPixel(258); // halfway*/ + expect(yScale.getPixelForValue(1)).toBeCloseToPixel(32); // right - paddingRight + expect(yScale.getPixelForValue(-1)).toBeCloseToPixel(484); // left + paddingLeft + expect(yScale.getPixelForValue(0)).toBeCloseToPixel(258); // halfway*/ expect(yScale.getValueForPixel(32)).toBeCloseTo(1, 1e-2); expect(yScale.getValueForPixel(484)).toBeCloseTo(-1, 1e-2); diff --git a/test/specs/scale.logarithmic.tests.js b/test/specs/scale.logarithmic.tests.js index f8f1d9cea00..fa0dc3caeeb 100644 --- a/test/specs/scale.logarithmic.tests.js +++ b/test/specs/scale.logarithmic.tests.js @@ -747,7 +747,7 @@ describe('Logarithmic Scale tests', function() { } }); - expect(chart.scales.yScale0.getLabelForIndex(0, 2)).toBe(150); + expect(chart.scales.yScale0.getLabelForValue(150)).toBe(150); }); describe('when', function() { @@ -884,8 +884,8 @@ describe('Logarithmic Scale tests', function() { var start = chart.chartArea[chartStart]; var end = chart.chartArea[chartEnd]; - expect(scale.getPixelForValue(firstTick, 0, 0)).toBeCloseToPixel(start); - expect(scale.getPixelForValue(lastTick, 0, 0)).toBeCloseToPixel(end); + expect(scale.getPixelForValue(firstTick)).toBeCloseToPixel(start); + expect(scale.getPixelForValue(lastTick)).toBeCloseToPixel(end); expect(scale.getValueForPixel(start)).toBeCloseTo(firstTick, 4); expect(scale.getValueForPixel(end)).toBeCloseTo(lastTick, 4); @@ -897,8 +897,8 @@ describe('Logarithmic Scale tests', function() { start = chart.chartArea[chartEnd]; end = chart.chartArea[chartStart]; - expect(scale.getPixelForValue(firstTick, 0, 0)).toBeCloseToPixel(start); - expect(scale.getPixelForValue(lastTick, 0, 0)).toBeCloseToPixel(end); + expect(scale.getPixelForValue(firstTick)).toBeCloseToPixel(start); + expect(scale.getPixelForValue(lastTick)).toBeCloseToPixel(end); expect(scale.getValueForPixel(start)).toBeCloseTo(firstTick, 4); expect(scale.getValueForPixel(end)).toBeCloseTo(lastTick, 4); diff --git a/test/specs/scale.radialLinear.tests.js b/test/specs/scale.radialLinear.tests.js index ee83eb96cd5..a474a5f22b3 100644 --- a/test/specs/scale.radialLinear.tests.js +++ b/test/specs/scale.radialLinear.tests.js @@ -157,25 +157,21 @@ describe('Test the radial linear scale', function() { }); it('Should ensure that the scale has a max and min that are not equal', function() { - var scaleID = 'myScale'; - - var mockData = { - datasets: [], - labels: [] - }; - - var mockContext = window.createMockContext(); - var Constructor = Chart.scaleService.getScaleConstructor('radialLinear'); - var scale = new Constructor({ - ctx: mockContext, - options: Chart.scaleService.getScaleDefaults('radialLinear'), // use default config for scale - chart: { - data: mockData + var chart = window.acquireChart({ + type: 'radar', + data: { + datasets: [], + labels: [] }, - id: scaleID, + options: { + scale: { + id: 'myScale' + } + } }); - scale.update(200, 300); + var scale = chart.scales.myScale; + expect(scale.min).toBe(-1); expect(scale.max).toBe(1); }); @@ -428,7 +424,7 @@ describe('Test the radial linear scale', function() { } } }); - expect(chart.scale.getLabelForIndex(1, 0)).toBe(5); + expect(chart.scale.getLabelForValue(5)).toBe(5); }); it('should get the correct distance from the center point', function() { diff --git a/test/specs/scale.time.tests.js b/test/specs/scale.time.tests.js index 604bc9307bf..8f84ad0f554 100755 --- a/test/specs/scale.time.tests.js +++ b/test/specs/scale.time.tests.js @@ -1,24 +1,25 @@ // Time scale tests describe('Time scale tests', function() { function createScale(data, options, dimensions) { - var scaleID = 'myScale'; - var mockContext = window.createMockContext(); - var Constructor = Chart.scaleService.getScaleConstructor('time'); var width = (dimensions && dimensions.width) || 400; var height = (dimensions && dimensions.height) || 50; - var scale = new Constructor({ - ctx: mockContext, - options: options, - chart: { - data: data, - width: width, - height: height - }, - id: scaleID - }); - scale.update(width, height); - return scale; + options = options || {}; + options.type = 'time'; + options.id = 'xScale0'; + + var chart = window.acquireChart({ + type: 'line', + data: data, + options: { + scales: { + xAxes: [options] + } + } + }, {canvas: {width: width, height: height}}); + + + return chart.scales.xScale0; } function getLabels(scale) { @@ -538,7 +539,7 @@ describe('Time scale tests', function() { xAxes: [{ id: 'xScale0', type: 'time', - position: 'bottom' + position: 'bottom', }], } } @@ -564,14 +565,14 @@ describe('Time scale tests', function() { var lastPointOffsetMs = moment(chart.config.data.labels[chart.config.data.labels.length - 1]).valueOf() - scale.min; var lastPointPixel = scale.left + lastPointOffsetMs / msPerPix; - expect(scale.getPixelForValue('', 0, 0)).toBeCloseToPixel(firstPointPixel); + expect(scale.getPixelForValue('2015-01-01T20:00:00')).toBeCloseToPixel(firstPointPixel); expect(scale.getPixelForValue(chart.data.labels[0])).toBeCloseToPixel(firstPointPixel); expect(scale.getValueForPixel(firstPointPixel)).toBeCloseToTime({ value: moment(chart.data.labels[0]), unit: 'hour', }); - expect(scale.getPixelForValue('', 6, 0)).toBeCloseToPixel(lastPointPixel); + expect(scale.getPixelForValue('2015-01-10T12:00')).toBeCloseToPixel(lastPointPixel); expect(scale.getValueForPixel(lastPointPixel)).toBeCloseToTime({ value: moment(chart.data.labels[6]), unit: 'hour' @@ -654,16 +655,21 @@ describe('Time scale tests', function() { xAxes: [{ id: 'xScale0', type: 'time', - position: 'bottom' + position: 'bottom', + ticks: { + source: 'labels', + autoSkip: false + } }], } } }); var xScale = chart.scales.xScale0; - expect(xScale.getLabelForIndex(0, 0)).toBeTruthy(); - expect(xScale.getLabelForIndex(0, 0)).toBe('2015-01-01T20:00:00'); - expect(xScale.getLabelForIndex(6, 0)).toBe('2015-01-10T12:00'); + var controller = chart.getDatasetMeta(0).controller; + expect(xScale.getLabelForValue(controller._getParsed(0)[xScale.id])).toBeTruthy(); + expect(xScale.getLabelForValue(controller._getParsed(0)[xScale.id])).toBe('Jan 1, 2015, 8:00:00 pm'); + expect(xScale.getLabelForValue(xScale.getValueForPixel(xScale.getPixelForTick(6)))).toBe('Jan 10, 2015, 12:00:00 pm'); }); describe('when ticks.callback is specified', function() { @@ -825,8 +831,10 @@ describe('Time scale tests', function() { }); var xScale = chart.scales.xScale0; - expect(xScale.getLabelForIndex(0, 0)).toBeTruthy(); - expect(xScale.getLabelForIndex(0, 0)).toBe('2015-01-01T20:00:00'); + var controller = chart.getDatasetMeta(0).controller; + var value = controller._getParsed(0)[xScale.id]; + expect(xScale.getLabelForValue(value)).toBeTruthy(); + expect(xScale.getLabelForValue(value)).toBe('Jan 1, 2015, 8:00:00 pm'); }); it('should get the correct label for a timestamp', function() { @@ -853,7 +861,8 @@ describe('Time scale tests', function() { }); var xScale = chart.scales.xScale0; - var label = xScale.getLabelForIndex(0, 0); + var controller = chart.getDatasetMeta(0).controller; + var label = xScale.getLabelForValue(controller._getParsed(0)[xScale.id]); expect(label).toEqual('Jan 8, 2018, 5:14:23 am'); }); @@ -879,9 +888,9 @@ describe('Time scale tests', function() { }); var xScale = chart.scales.xScale0; - var pixel = xScale.getPixelForValue('', 0, 0); + var pixel = xScale.getPixelForValue('2016-05-27'); - expect(xScale.getValueForPixel(pixel).valueOf()).toEqual(moment(chart.data.labels[0]).valueOf()); + expect(xScale.getValueForPixel(pixel)).toEqual(moment(chart.data.labels[0]).valueOf()); }); it('does not create a negative width chart when hidden', function() {