From a564802d7029645acba4d47e7be6f67dd4b1bfea Mon Sep 17 00:00:00 2001 From: kurkle Date: Thu, 17 Oct 2019 00:19:32 +0300 Subject: [PATCH] Early data parsing + stacking by value determineDataLimits: simplify/fix for object data Add test for object data Make CC happier Review comments far far away some fixes Rebase, comments, cc warnings Some more cleanup get rid of _index --- src/controllers/controller.bar.js | 145 ++++-- src/controllers/controller.bubble.js | 12 +- src/controllers/controller.doughnut.js | 24 +- src/controllers/controller.line.js | 56 +-- src/core/core.datasetController.js | 458 ++++++++++++++++-- src/core/core.scale.js | 204 +++++--- src/scales/scale.category.js | 49 +- src/scales/scale.linear.js | 116 +---- src/scales/scale.linearbase.js | 12 +- src/scales/scale.logarithmic.js | 108 +---- src/scales/scale.radialLinear.js | 8 +- src/scales/scale.time.js | 247 +++++----- test/fixtures/controller.bar/data/object.js | 32 ++ test/fixtures/controller.bar/data/object.png | Bin 0 -> 2347 bytes .../floatBar/float-bar-stacked-horizontal.png | Bin 5070 -> 2103 bytes .../floatBar/float-bar-stacked.png | Bin 5396 -> 2199 bytes test/specs/scale.category.tests.js | 22 +- test/specs/scale.time.tests.js | 33 +- 18 files changed, 949 insertions(+), 577 deletions(-) create mode 100644 test/fixtures/controller.bar/data/object.js create mode 100644 test/fixtures/controller.bar/data/object.png diff --git a/src/controllers/controller.bar.js b/src/controllers/controller.bar.js index e56d03ec552..7b4c63a336c 100644 --- a/src/controllers/controller.bar.js +++ b/src/controllers/controller.bar.js @@ -126,6 +126,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 = {_index: i}; + 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, @@ -145,6 +196,36 @@ module.exports = DatasetController.extend({ 'minBarLength' ], + /** + * 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: {index: 1, xScale0: 0, yScale0: 1} + * @private + */ + _parsePrimitiveData: function() { + return parseArrayOrPrimitive.apply(this, arguments); + }, + + /** + * 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: {index: 1, xScale0: 0, yScale0: 1} + * @private + */ + _parseArrayData: function() { + return parseArrayOrPrimitive.apply(this, arguments); + }, + initialize: function() { var me = this; var meta, scaleOpts; @@ -294,7 +375,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._getParsedValue(scale, i))); } return { @@ -312,50 +393,36 @@ module.exports = DatasetController.extend({ */ calculateBarValuePixels: function(datasetIndex, 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 value = me._getParsedValue(valueScale, index); + var custom = me._getParsedCustom(index); + var length = me._getStackedValue(valueScale, index); + 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 { @@ -394,15 +461,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._getParsedValue(scale, i))) { rects[i].draw(); } } diff --git a/src/controllers/controller.bubble.js b/src/controllers/controller.bubble.js index 508e9bb3436..be7310d65d7 100644 --- a/src/controllers/controller.bubble.js +++ b/src/controllers/controller.bubble.js @@ -63,6 +63,13 @@ module.exports = DatasetController.extend({ 'rotation' ], + /** + * @private + */ + _parseCustomObjectData: function(obj) { + return obj && obj.r && +obj.r; + }, + /** * @protected */ @@ -87,11 +94,10 @@ module.exports = DatasetController.extend({ var xScale = me.getScaleForId(meta.xAxisID); var yScale = me.getScaleForId(meta.yAxisID); var options = me._resolveDataElementOptions(point, 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 x = reset ? xScale.getPixelForDecimal(0.5) : xScale.getPixelForValue(me._getParsedValue(xScale, index)); + var y = reset ? yScale.getBasePixel() : yScale.getPixelForValue(me._getParsedValue(yScale, index)); point._xScale = xScale; point._yScale = yScale; diff --git a/src/controllers/controller.doughnut.js b/src/controllers/controller.doughnut.js index 7cee5444457..55219a9e043 100644 --- a/src/controllers/controller.doughnut.js +++ b/src/controllers/controller.doughnut.js @@ -137,6 +137,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; @@ -223,7 +236,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 || {}; @@ -267,14 +280,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 dc1fc689b24..f6253152978 100644 --- a/src/controllers/controller.line.js +++ b/src/controllers/controller.line.js @@ -117,9 +117,7 @@ module.exports = DatasetController.extend({ var me = this; var meta = me.getMeta(); var custom = point.custom || {}; - 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; @@ -127,8 +125,8 @@ module.exports = DatasetController.extend({ var options = me._resolveDataElementOptions(point, index); - x = xScale.getPixelForValue(typeof value === 'object' ? value : NaN, index, datasetIndex); - y = reset ? yScale.getBasePixel() : me.calculatePointY(value, index, datasetIndex); + x = xScale.getPixelForValue(me._getParsedValue(xScale, index)); + y = reset ? yScale.getBasePixel() : yScale.getPixelForValue(me._getStackedValue(yScale, index)); // Utility point._xScale = xScale; @@ -177,42 +175,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); - }, - updateBezierControlPoints: function() { var me = this; var chart = me.chart; @@ -319,4 +281,18 @@ module.exports = DatasetController.extend({ model.borderWidth = valueOrDefault(options.hoverBorderWidth, options.borderWidth); model.radius = valueOrDefault(options.hoverRadius, options.radius); }, + + // DEPRECATIONS + + /** + * Provided for backward compatibility + * @deprecated since version 2.9.0 + * @todo remove at version 3 + */ + calculatePointY: function(value, index, datasetIndex) { + var me = this; + var chart = me.chart; + var controller = datasetIndex === me.index ? me : chart.getDatasetMeta(datasetIndex).controller; + return controller._getStackedValue(me._yScale, index); + }, }); diff --git a/src/core/core.datasetController.js b/src/core/core.datasetController.js index 2d8972e6e2f..c52814be075 100644 --- a/src/core/core.datasetController.js +++ b/src/core/core.datasetController.js @@ -75,6 +75,73 @@ 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 getStackKey(xScale, yScale, meta) { + var stacked = yScale.options.stacked; + return (stacked || (stacked === undefined && meta.stack !== undefined)) + && 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; +} + // Base class for all dataset controllers (line, bar, etc) var DatasetController = function(chart, datasetIndex) { this.initialize(chart, datasetIndex); @@ -125,11 +192,13 @@ 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(); me.addElements(); - me._type = me.getMeta().type; }, updateIndex: function(datasetIndex) { @@ -138,7 +207,7 @@ helpers.extend(DatasetController.prototype, { linkScales: function() { var me = this; - var meta = me.getMeta(); + var meta = me._cachedMeta; var chart = me.chart; var scales = chart.scales; var dataset = me.getDataset(); @@ -168,14 +237,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,53 +289,101 @@ helpers.extend(DatasetController.prototype, { return type && new type({ _chart: me.chart, _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 + // TODO: Utilize Proxy instead, after IE support is dropped + 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); } meta.dataset = meta.dataset || me.createMetaDataset(); + me._parse(0, ilen); }, 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 = []); - - // 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; - } + var dataChanged = me._dataCheck(); + var labelsChanged = me._labelCheck(); + var scaleChanged = me._scaleCheck(); // 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 +404,275 @@ 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 + */ + _getParsedValue: function(scale, index) { + var data = this._cachedMeta.data; + if (index < 0 || index >= data.length) { + return; + } + return data[index]._parsed[scale.id]; + }, + + /** + * @private + */ + _getParsedCustom: function(index) { + var data = this._cachedMeta.data; + if (index < 0 || index >= data.length) { + return; + } + return data[index]._parsed._custom; + }, + + /** + * @private + */ + _getStackedValue: function(scale, index) { + var chart = this.chart; + var meta = this._cachedMeta; + var parsed = meta.data[index]._parsed; + var value = parsed[scale.id]; + var stacked = scale.options.stacked; + var stack; + + if (stacked || (stacked === undefined && meta.stack !== undefined)) { + stack = { + keys: getSortedDatasetIndices(chart, true), + values: chart._xref[parsed._stackKeys[scale.id]] + }; + value = applyStack(stack, value, meta.index); + } + return value; + }, + + _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 = scale.options.stacked; + var indices = getSortedDatasetIndices(chart, true); + var i, item, value, parsed, stack, min, minPositive; + + stacked = canStack && (stacked || (stacked === undefined && meta.stack !== undefined)); + 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 +687,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 +709,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; @@ -509,17 +884,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); } }, @@ -530,6 +907,7 @@ helpers.extend(DatasetController.prototype, { for (var i = 0; i < count; ++i) { this.addElementAndReset(start + i); } + this._parse(start, count); }, /** @@ -544,21 +922,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 f292bd2c2e5..7a3763e6b32 100644 --- a/src/core/core.scale.js +++ b/src/core/core.scale.js @@ -327,6 +327,78 @@ var Scale = Element.extend({ zeroLineIndex: 0, + /** + * Parse a supported input value to internal representation. + * @param {*} raw + * @param {number} index + * @private + * @since 2.9 + */ + _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 2.9 + */ + _parseObject: function(obj, axis, index) { + if (obj[axis] !== undefined) { + return this._parse(obj[axis], index); + } + return null; + }, + + /** + * Get internal parsed value + * @param {number} index + * @param {number} datasetIndex + * @private + * @since 2.9 + */ + _getParsedValue: function(index, datasetIndex) { + return this.chart.getDatasetMeta(datasetIndex).controller._getParsedValue(this, index); + }, + + /** + * Get internal parsed custom data + * @param {number} index + * @param {number} datasetIndex + * @private + * @since 2.9 + */ + _getParsedCustom: function(index, datasetIndex) { + return this.chart.getDatasetMeta(datasetIndex).controller._getParsedCustom(index); + }, + + _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 @@ -770,32 +842,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; var labels, i, ilen; @@ -836,43 +882,16 @@ var Scale = Element.extend({ return labelSizes; }, - /** - * @private - */ - _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 + ']'; + _getScaleLabel: function(index, datasetIndex) { + var c = this._getParsedCustom(index, datasetIndex); + if (c && c.start !== undefined && c.end !== undefined) { + return '[' + c.start + ', ' + c.end + ']'; } - return +this.getRightValue(rawValue); + return this._getParsedValue(index, datasetIndex); }, /** @@ -1014,25 +1033,43 @@ 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 this._getMatchingVisibleMetas().length > 0; + }, + + /** + * @private + */ + _getStackKey: function(meta) { + return [ + meta.type, + // we have a separate stack for stack=undefined datasets when the opts.stacked is undefined + ((this.options.stacked === undefined && meta.stack === undefined) ? meta._datasetIndex : ''), + meta.stack + ].join('.'); + }, + + /** + * @private + */ + _hasStacks: function(metas) { + var stacked = this.options.stacked; + var i, ilen; + + if (stacked !== undefined) { + return stacked; } + for (i = 0, ilen = metas.length; i < ilen; ++i) { + if (metas[i].stack !== undefined) { + return true; + } + } return false; }, @@ -1427,14 +1464,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/scales/scale.category.js b/src/scales/scale.category.js index 29dbf649314..74c34728928 100644 --- a/src/scales/scale.category.js +++ b/src/scales/scale.category.js @@ -4,12 +4,26 @@ 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(); @@ -54,13 +68,13 @@ module.exports = Scale.extend({ getLabelForIndex: function(index, datasetIndex) { var me = this; - var chart = me.chart; + var value = me._getParsedValue(index, datasetIndex); + 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() { @@ -86,34 +100,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) { var me = this; - var valueCategory, labels, idx; if (!isNullOrUndef(index) && !isNullOrUndef(datasetIndex)) { - value = me.chart.data.datasets[datasetIndex].data[index]; + value = me._getParsedValue(index, datasetIndex); + } else if (typeof value !== 'number') { + value = me._parse(value, index); } - // 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 1c3da726d7d..8878a1faab0 100644 --- a/src/scales/scale.linear.js +++ b/src/scales/scale.linear.js @@ -11,111 +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 (opts.relativePoints) { - pos[i] = 100; - } else 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; - } - } - - 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); - } + 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; } - helpers.each(stacks, function(stackValues) { - values = stackValues.pos.concat(stackValues.neg); - me.min = Math.min(me.min, helpers.min(values)); - me.max = Math.max(me.max, helpers.max(values)); - }); - - 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(); }, @@ -141,13 +53,13 @@ module.exports = LinearScaleBase.extend({ }, getLabelForIndex: function(index, datasetIndex) { - return this._getScaleLabel(this.chart.data.datasets[datasetIndex].data[index]); + return this._getScaleLabel(index, datasetIndex); }, // 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 95ba9bebe1f..6df00be3836 100644 --- a/src/scales/scale.linearbase.js +++ b/src/scales/scale.linearbase.js @@ -85,11 +85,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 6a6553ab8a9..1a620dff985 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,103 +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; - } - } - } + var minmax = me._getMinMax(true); + var min = minmax.min; + var max = minmax.max; + var minPositive = minmax.minPositive; - helpers.each(valuesPerStack, function(valuesForType) { - if (valuesForType.length > 0) { - var minVal = helpers.min(valuesForType); - var maxVal = helpers.max(valuesForType); - me.min = Math.min(me.min, minVal); - me.max = Math.max(me.max, maxVal); - } - }); + 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; - } 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; - - // Common base implementation to handle ticks.min, ticks.max - this.handleTickRangeOptions(); + me.handleTickRangeOptions(); }, handleTickRangeOptions: function() { @@ -242,7 +160,7 @@ module.exports = Scale.extend({ // Get the correct tooltip label getLabelForIndex: function(index, datasetIndex) { - return this._getScaleLabel(this.chart.data.datasets[datasetIndex].data[index]); + return this._getScaleLabel(index, datasetIndex); }, getPixelForTick: function(index) { @@ -287,8 +205,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 ea77ae0226c..798ce0b9c2b 100644 --- a/src/scales/scale.radialLinear.js +++ b/src/scales/scale.radialLinear.js @@ -313,9 +313,9 @@ module.exports = LinearScaleBase.extend({ 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) { + helpers.each(meta.data, function(metaData, index) { + var value = me._getParsedValue(index, datasetIndex); + if (isNaN(value) || metaData.hidden) { return; } @@ -350,7 +350,7 @@ module.exports = LinearScaleBase.extend({ }, getLabelForIndex: function(index, datasetIndex) { - return +this.getRightValue(this.chart.data.datasets[datasetIndex].data[index]); + return this._getParsedValue(index, datasetIndex); }, fit: function() { diff --git a/src/scales/scale.time.js b/src/scales/scale.time.js index 49377b480a9..5a0409371df 100644 --- a/src/scales/scale.time.js +++ b/src/scales/scale.time.js @@ -10,7 +10,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 = { @@ -229,7 +228,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; } @@ -390,6 +389,79 @@ 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; +} + +function filterBetween(timestamps, min, max) { + var ticks = []; + var i, ilen, timestamp; + + // 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); + } + } + return ticks; +} + var defaultConfig = { position: 'bottom', @@ -441,12 +513,28 @@ var defaultConfig = { }; module.exports = Scale.extend({ - initialize: function() { - this.mergeTicksOptions(); - Scale.prototype.initialize.call(this); + _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 = {}); @@ -457,150 +545,79 @@ module.exports = Scale.extend({ deprecated('time scale', time.min, 'time.min', 'ticks.min'); deprecated('time scale', time.max, 'time.max', 'ticks.max'); + me._cache = {}; + me.mergeTicksOptions(); + // 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); - }, - - /** - * 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 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])); - } - - 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]); + 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]); } - 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, getMin(options)) || min; max = parse(me, getMax(options)) || 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, getMin(options)) || timestamps[0]; + me.max = parse(me, getMax(options)) || timestamps[timestamps.length - 1]; } - // Enforce limits with user min/max options - min = parse(me, getMin(options)) || min; - max = parse(me, getMax(options)) || 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) { @@ -613,21 +630,18 @@ module.exports = Scale.extend({ getLabelForIndex: function(index, datasetIndex) { 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]; + var labels = me._getLabels(); + var label = labels && index < labels.length && labels[index]; + var value = me._getParsedValue(index, datasetIndex); - if (helpers.isObject(value)) { - label = me.getRightValue(value); - } if (timeOpts.tooltipFormat) { - return adapter.format(toTimestamp(me, label), timeOpts.tooltipFormat); + return adapter.format(value, timeOpts.tooltipFormat); } if (typeof label === 'string') { return label; } - return adapter.format(toTimestamp(me, label), timeOpts.displayFormats.datetime); + return adapter.format(value, timeOpts.displayFormats.datetime); }, /** @@ -683,7 +697,8 @@ module.exports = Scale.extend({ var time = null; if (index !== undefined && datasetIndex !== undefined) { - time = me._timestamps.datasets[datasetIndex][index]; + time = me._getParsedValue(index, datasetIndex) || + getLabelTimestamps(me)[index]; } if (time === null) { 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 0000000000000000000000000000000000000000..c705448f0d10e3b9308a40b233cf35adf66cbf17 GIT binary patch literal 2347 zcmeAS@N?(olHy`uVBq!ia0y~yU;;83893O0)X@p&(is>yK6$!0hE&XXd&BTl>s8U# zhnGE0g_LPtTH>X=Eo_>k!vV${OL!$46SQyKuxxnXFsC`WfG_^lh6my{MepO@JpXw^ zI6_8G|GP{>@wv)#{Ldr5|NZptCgX#>EDQ`fDhvz>ii`{l+?)&y5grT-2b_S`iwQ9> zY?#2n(9lYCC3h;${XAZLuKe`7+2!x~uCp){Jhps(`}v&on}2`3yZdk!14AAMgN7tmx`=;NI?r#3C28)9zo9EZJ@1JwM z{{Ft|Z}(LgAc60|&1f(g7CE{s2Yg_Ww?%-#m=hYg7n~T*jA|Gzp>!bNyygGeb3ZF@ z{(asI&Xv$uceZ{0$LPV^J-_OHB!F`!BxM{3fTamYg5Xhwrkzn~NI6H>V7Xj;F8sWu zz3y&#dx z_}fc7JMteqVJpLxnUQYfidV(KNH@Lh-r#^NQ=@ jD(3G81|DwE@c+C0sh(?PCOfC@1*!6M^>bP0l+XkKWE&^h literal 0 HcmV?d00001 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 5df57310985aac9a184543beed66eb856ac5c229..c3066fd6812b3ece9fe2526b933a1e6cc5e1fef1 100644 GIT binary patch literal 2103 zcmeAS@N?(olHy`uVBq!ia0y~yU;;83893O0)X@p&(is@o|9QGNhE&XXd(AK}BwXZp zVzTmv__IteUK$+|S{ZY}FEdE)R>KP0f>! zm9xGtdVb{j?*XT|8Lu^ul1KsU*Kw?9|gx5KVl?s~dmcKo@0 zUn2`6Ziso569ssh7Q2UO8#6JUGkFlmAws%0K`( zAG!Yd@Qx_I&0??r?mYi7MVeo1_Kasa`O@sR_fEzXZrUfm`fG{$w?}6W$L{xMsB8F9 z^i6f$`J`v_&e!%m2F9*j{JkS{Z2uN)e)ZAsSyy+s{^LibcP7@}`}&*Z0lNUhUrq*w zOTwcR$wAN!%v*(9=cMUg`&~T$@kui|9Z+$6&y25X&*YttQflqPUcdBvb98q5?z;H| zqL_iJlrpSKJv^<*jRpY4DpLyxGEuwOgc5&a1KK^gre#-`tN59^r#>{Um zz4Ly*OU#~g8~OG>|8e(weZPMGBfU2-fA63F{?}|~5698mN?D0SI4aQN^VLuO47a_l T+X9$QfuuZL{an^LB{Ts59S3X1 literal 5070 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 Jvd$@?2>`dp#9;sc literal 5396 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 OTDR8Gy1>dS=HCF`JO$wZ diff --git a/test/specs/scale.category.tests.js b/test/specs/scale.category.tests.js index e49fff65a21..8b18e9a9602 100644 --- a/test/specs/scale.category.tests.js +++ b/test/specs/scale.category.tests.js @@ -331,20 +331,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, 0, 0)).toBeCloseToPixel(257); + expect(yScale.getValueForPixel(257)).toBe(2); - expect(yScale.getPixelForValue(0, 4, 0)).toBeCloseToPixel(484); - expect(yScale.getValueForPixel(484)).toBe(4); + expect(yScale.getPixelForValue(0, 4, 0)).toBeCloseToPixel(144); + 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, 0, 0)).toBeCloseToPixel(256); + expect(yScale.getValueForPixel(256)).toBe(2); - expect(yScale.getPixelForValue(0, 4, 0)).toBeCloseToPixel(437); - expect(yScale.getValueForPixel(437)).toBe(4); + expect(yScale.getPixelForValue(0, 4, 0)).toBeCloseToPixel(166); + expect(yScale.getValueForPixel(167)).toBe(1); }); it('should get the correct pixel for a value when vertical and zoomed', function() { @@ -381,13 +381,13 @@ 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(0, 1, 0)).toBeCloseToPixel(707); + expect(yScale.getPixelForValue(0, 3, 0)).toBeCloseToPixel(482); yScale.options.offset = true; chart.update(); - expect(yScale.getPixelForValue(0, 1, 0)).toBeCloseToPixel(107); + expect(yScale.getPixelForValue(0, 1, 0)).toBeCloseToPixel(557); expect(yScale.getPixelForValue(0, 3, 0)).toBeCloseToPixel(407); }); diff --git a/test/specs/scale.time.tests.js b/test/specs/scale.time.tests.js index 71967491d4c..09f2557bd34 100755 --- a/test/specs/scale.time.tests.js +++ b/test/specs/scale.time.tests.js @@ -1,22 +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 scale = new Constructor({ - ctx: mockContext, - options: options, - chart: { - data: data - }, - id: scaleID - }); - var width = (dimensions && dimensions.width) || 400; var height = (dimensions && dimensions.height) || 50; - 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 getTicksLabels(scale) { @@ -825,7 +828,7 @@ 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'); + expect(xScale.getLabelForIndex(0, 0)).toBe('Jan 1, 2015, 8:00:00 pm'); }); it('should get the correct label for a timestamp', function() {