From 72df272234c13f90a7eef6e519209bd97880a5d8 Mon Sep 17 00:00:00 2001 From: Jukka Kurkela Date: Tue, 5 Nov 2019 01:07:01 +0200 Subject: [PATCH] Early data parsing, stacking by value and support object data (#6576) * Early data parsing + stacking by value * Review comments * review comments * Remove reduntant parsing * Couple CC warnings * Optimize filterBetween * More migration info --- docs/developers/axes.md | 4 +- docs/getting-started/v3-migration.md | 11 +- src/controllers/controller.bar.js | 147 ++++-- src/controllers/controller.bubble.js | 36 +- src/controllers/controller.doughnut.js | 24 +- src/controllers/controller.line.js | 50 +- src/controllers/controller.polarArea.js | 2 - src/controllers/controller.radar.js | 2 - src/core/core.datasetController.js | 467 ++++++++++++++++-- src/core/core.scale.js | 166 +++---- src/core/core.tooltip.js | 5 +- src/scales/scale.category.js | 54 +- src/scales/scale.linear.js | 115 +---- src/scales/scale.linearbase.js | 12 +- src/scales/scale.logarithmic.js | 108 +--- src/scales/scale.radialLinear.js | 30 +- src/scales/scale.time.js | 269 +++++----- 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 .../fixtures/controller.bubble/radius-data.js | 47 ++ .../controller.bubble/radius-data.png | Bin 0 -> 1722 bytes test/specs/scale.category.tests.js | 67 ++- test/specs/scale.linear.tests.js | 14 +- test/specs/scale.logarithmic.tests.js | 10 +- test/specs/scale.radialLinear.tests.js | 30 +- test/specs/scale.time.tests.js | 63 ++- 28 files changed, 1038 insertions(+), 727 deletions(-) create mode 100644 test/fixtures/controller.bar/data/object.js create mode 100644 test/fixtures/controller.bar/data/object.png create mode 100644 test/fixtures/controller.bubble/radius-data.js create mode 100644 test/fixtures/controller.bubble/radius-data.png diff --git a/docs/developers/axes.md b/docs/developers/axes.md index 682e93ea9..4b919815c 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 80f201393..8587554cf 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 f7a4b2216..4bf0010eb 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 87db319a8..48c0a2ee4 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 7495d15fa..099c88b91 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 a8e242d34..41066c011 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 b73a938bb..d2458bd7b 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 a690c75d0..1a0bee2fa 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 d885c53c5..a6e889d3e 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 f3521ee12..6cc8b3ab3 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 f1aeee298..38d4e990d 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 487f4b554..cb07a6751 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 21999c2be..84c3b19c5 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 b20820aaf..c420b6019 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 43a9d46ab..f85ed9549 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 18216168d..5ccf9a20d 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 453d18023..36024c1d1 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 000000000..f5b6ac225 --- /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 zc-rd>@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!14AAMgN7 zh9e404SZk+BndD&u#Xx?W#cY9vixs#{^$2izaQP*{9g@bPL$2_>)ZFwxn6&NU-h^9 zDo_I)xET#5!z|EcIp70xz!m`pV@{ZfP7G&8H4GOG2LjGp{;xgvv-0NO=gnaKP|eP^ z&;J-bc)RCU-H!yY7MOaN(GYWaRG}u0N)Lea<>GVU=Pm7Zcgx#LJbC{d$l9}i-@B`S z?ao`?=dZ9{zi&NI{59kIXIH=f<(_}%{iivfpFf{dueZnFUgGI-#{B13zh4Cw;|%Y= z&G~%u^S}DOdxi1)s;e0)r0H054t*sTiqGAjS6mlYF@HZW@Nk2M|KIIT^;|16**SGD Oi0|p@=d#Wzp$PzF8z@N?(olHy`uVBq!ia0y~yU;;83893O0)X@p&(is@o|9QGNhE&XXd(AK}BwXZp zVzTmv__IteUK$+|S{ZY}FEdE)R>KP0f>! zm9xGtdVb{j?d~Xp8KO6z@4SE7|9zg=@#*G<>GtvezNW{_Ke}h`zsrw*vvYYDOgZc3 z%fMj5*|+KD3QmRwwS$3X(x!|I7gQ`xr7c!tV3@_?elzC^3xmVt2O6_`XE88j2+5>w z4isi!@NHV0W0u9lu)uTF5K0UYUi~#CEJ^>K&7A)SkL*3Fw#>5nj~Ud`wL{u198W~LEaMM2d)n7~0zdbs8ICj53LtVpz96Sj?Ql1T{oXl95ej! zcb#*mfA!N8+3%T_N7KG)9JTs0Fa5SfR8G<^?%UDF|E=3^*&y=h*PGOs`K_gQ-tTvb z*>i3q-~Q)6?tZWD*Ux{X_vYpA{qx`dn$7ItIGS52(?~cf(4+6wPyP(Iy{+2zopr038Qw#sB~S literal 5070 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 T9PdakfPcL8&g-t&^AG(4ZU`=W diff --git a/test/fixtures/controller.bar/floatBar/float-bar-stacked.png b/test/fixtures/controller.bar/floatBar/float-bar-stacked.png index c9c02ea2f7b4136a9107bebc0b3512fcce66731a..5da257bcd43fdbec720bbd0c36c92ebb67919fb2 100644 GIT binary patch literal 2199 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 diff --git a/test/fixtures/controller.bubble/radius-data.js b/test/fixtures/controller.bubble/radius-data.js new file mode 100644 index 000000000..0c90917fd --- /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 0000000000000000000000000000000000000000..ac819c21e45631137ef113206d0f283a4a4fdf46 GIT binary patch literal 1722 zc-m!DdtB1z8h=4%jwsnNkYlE9b6%R6o~)QsGs_gFmSk<33?1`+pSQ5^2JM(xo`dBa z<>hFar9i_2Dx|{4KH4-diN!chyQrZtM8y(CWWVjNZRh#p`MmGvd7tO=JkR(0elML2 z3D}0TK>`4_5d)8r0f0eAP-tNe`59q56fiF$9y=0t?xtpva>lkFBVGC%{6ohJ-{8Uz zI2}RNF_-Jkh}UVQ3-i3!<#fy#-0gmmbog1{KXb~I76?J+mttj)msW>l3K?fGuVy+ekBG`1&!V>z+V)mUg4FIOO&zjDKhX zceU4}EJbBeXNkpPyMyFerVVKiuCJ=L*dcym7~`8Zd1d;X>aMH+8=+>%rh$idY;3F@ zHS#SL>mM3M9#Duf%|}1HI(>ilpx`NDbUtf$fV0J)v!wC=BJ(i* zG9>6|Y-)4!xw}uVBk*neA1m}bb1$N;Ue27FeOHEr>oq4yDT?pGn3vIkh2t9}SB_6x z9znMTgs!A;n$8p17iyd4zue)U#&kHhr^5lG55|?(&((guZPSssz4E*7KIQ9Z<#vZS zr%5S)r{#-(S*mv_9l+^mXhZ>)60{qv(;{7kDP?P&E~T&CmI?fq_^E^gH`=oEZz+?9 z6S;gsSk~bILF-F=evd$~9Nx1Pv6vwz5A1)odXS=EDC50e`47dtU0YlGgqzJZvMxVP zZ7)FkT^M^d237{UpdNVp^dldX*0&dL^NZ5!yL*&F^y0y(f3&u?Zv2?D2Z%GNNk;7r z)MjQm+=Na>!h4UB?npif$~ujsU1g!=q9pjq3UZ>MY0#uAht;UM?oq4?^A^Sr1%^47 z);C8g7%>tT@Ma|o?lLG?W)$@1go4D^y~yW{8V(qDY>d=CvGc8rc``P}rb(Q@8wMvP z{Dc$WzA1WMd@8EBjLYL~b_I->A~Uw5ZX9p$q_*>=7?Ux<8;NRhuz0FIfa`*gr#)M> zn%ECIV9?l-YDAf5KTrLQ3V0xIT}z5k_m=OnbT6$Z$ydxwYekRu%^Egi*^ZLFs@)?x z>IZ{@2K?3BkY}rMGk}Y>@++C|?Ck8n9k)^7%sS@~Vsmk7{8}B~A>9z@+TayzdBrmz z8kbM=u?I{YQ8GDos30#rHulvInp*(XX9@Jej zArRpIESs2Q7>crm(I(&fyXr`x!0sx*4VsLv=uh`ks_PQ_{IDdN6}z(iBB^}U0;Is zlIpD=;SnpyJf*vZFT=9TlajVU!IJLmdH=z#34d5~bZY9tQa1mnNf^kUVMgsk)Hok18Wx@uU^8fR5Ve(fV$ii(O4awG~SF3YGp{4ofb zSW^tLWUIy@yJ|ChzJBqAHbr4p=W_}I5aOS}?c?8msUL`Bs_1)D1<26|dyDrMlsq%( z{?T?kJfOb_~@t7KpQK+pwChbp5PBhJi-MIQXzBCCBFjIc zH^-N}oS8sGuwm^FclBp{*_W|#n8?;Pxa1jD=H)6;1>6HbT*>_5`%9j$9zv3qtb+86 zGt_iG+^WKD@akNsMVUwA_}kNz(#{q%E}C?Q+v1Jwio7RQz);&oy29G{O;8IT15a{En?YV7kEzmPakR!NHsqZlDJNEZoMAsfJ l*d$>52=}T{f9TqmzhPaCm~$C6rr>|O6a7Pu@%*BSe+7`>F3