diff --git a/src/controllers/controller.line.js b/src/controllers/controller.line.js index b28997aad07..3ddd09896e0 100644 --- a/src/controllers/controller.line.js +++ b/src/controllers/controller.line.js @@ -1,7 +1,7 @@ import DatasetController from '../core/core.datasetController'; import {isNullOrUndef} from '../helpers'; -import {_limitValue, isNumber} from '../helpers/helpers.math'; -import {_lookupByKey} from '../helpers/helpers.collection'; +import {isNumber} from '../helpers/helpers.math'; +import {_getStartAndCountOfVisiblePoints, _scaleRangesChanged} from '../helpers/helpers.extras'; export default class LineController extends DatasetController { @@ -16,12 +16,12 @@ export default class LineController extends DatasetController { const {dataset: line, data: points = [], _dataset} = meta; // @ts-ignore const animationsDisabled = this.chart._animationsDisabled; - let {start, count} = getStartAndCountOfVisiblePoints(meta, points, animationsDisabled); + let {start, count} = _getStartAndCountOfVisiblePoints(meta, points, animationsDisabled); this._drawStart = start; this._drawCount = count; - if (scaleRangesChanged(meta)) { + if (_scaleRangesChanged(meta)) { start = 0; count = points.length; } @@ -133,54 +133,3 @@ LineController.overrides = { }, } }; - -function getStartAndCountOfVisiblePoints(meta, points, animationsDisabled) { - const pointCount = points.length; - - let start = 0; - let count = pointCount; - - if (meta._sorted) { - const {iScale, _parsed} = meta; - const axis = iScale.axis; - const {min, max, minDefined, maxDefined} = iScale.getUserBounds(); - - if (minDefined) { - start = _limitValue(Math.min( - _lookupByKey(_parsed, iScale.axis, min).lo, - animationsDisabled ? pointCount : _lookupByKey(points, axis, iScale.getPixelForValue(min)).lo), - 0, pointCount - 1); - } - if (maxDefined) { - count = _limitValue(Math.max( - _lookupByKey(_parsed, iScale.axis, max, true).hi + 1, - animationsDisabled ? 0 : _lookupByKey(points, axis, iScale.getPixelForValue(max), true).hi + 1), - start, pointCount) - start; - } else { - count = pointCount - start; - } - } - - return {start, count}; -} - -function scaleRangesChanged(meta) { - const {xScale, yScale, _scaleRanges} = meta; - const newRanges = { - xmin: xScale.min, - xmax: xScale.max, - ymin: yScale.min, - ymax: yScale.max - }; - if (!_scaleRanges) { - meta._scaleRanges = newRanges; - return true; - } - const changed = _scaleRanges.xmin !== xScale.min - || _scaleRanges.xmax !== xScale.max - || _scaleRanges.ymin !== yScale.min - || _scaleRanges.ymax !== yScale.max; - - Object.assign(_scaleRanges, newRanges); - return changed; -} diff --git a/src/controllers/controller.scatter.js b/src/controllers/controller.scatter.js index 1d1ccb73e5a..ee229120bc0 100644 --- a/src/controllers/controller.scatter.js +++ b/src/controllers/controller.scatter.js @@ -1,7 +1,125 @@ -import LineController from './controller.line'; +import DatasetController from '../core/core.datasetController'; +import {isNullOrUndef} from '../helpers'; +import {isNumber} from '../helpers/helpers.math'; +import {_getStartAndCountOfVisiblePoints, _scaleRangesChanged} from '../helpers/helpers.extras'; +import registry from '../core/core.registry'; -export default class ScatterController extends LineController { +export default class ScatterController extends DatasetController { + update(mode) { + const meta = this._cachedMeta; + const {data: points = []} = meta; + // @ts-ignore + const animationsDisabled = this.chart._animationsDisabled; + let {start, count} = _getStartAndCountOfVisiblePoints(meta, points, animationsDisabled); + this._drawStart = start; + this._drawCount = count; + + if (_scaleRangesChanged(meta)) { + start = 0; + count = points.length; + } + + if (this.options.showLine) { + + const {dataset: line, _dataset} = meta; + + // Update Line + line._chart = this.chart; + line._datasetIndex = this.index; + line._decimated = !!_dataset._decimated; + line.points = points; + + const options = this.resolveDatasetElementOptions(mode); + options.segment = this.options.segment; + this.updateElement(line, undefined, { + animated: !animationsDisabled, + options + }, mode); + } + + // Update Points + this.updateElements(points, start, count, mode); + } + + addElements() { + const {showLine} = this.options; + + if (!this.datasetElementType && showLine) { + this.datasetElementType = registry.getElement('line'); + } + + super.addElements(); + } + + updateElements(points, start, count, mode) { + const reset = mode === 'reset'; + const {iScale, vScale, _stacked, _dataset} = this._cachedMeta; + const firstOpts = this.resolveDataElementOptions(start, mode); + const sharedOptions = this.getSharedOptions(firstOpts); + const includeOptions = this.includeOptions(mode, sharedOptions); + const iAxis = iScale.axis; + const vAxis = vScale.axis; + const {spanGaps, segment} = this.options; + const maxGapLength = isNumber(spanGaps) ? spanGaps : Number.POSITIVE_INFINITY; + const directUpdate = this.chart._animationsDisabled || reset || mode === 'none'; + let prevParsed = start > 0 && this.getParsed(start - 1); + + for (let i = start; i < start + count; ++i) { + const point = points[i]; + const parsed = this.getParsed(i); + const properties = directUpdate ? point : {}; + const nullData = isNullOrUndef(parsed[vAxis]); + const iPixel = properties[iAxis] = iScale.getPixelForValue(parsed[iAxis], i); + const vPixel = properties[vAxis] = reset || nullData ? vScale.getBasePixel() : vScale.getPixelForValue(_stacked ? this.applyStack(vScale, parsed, _stacked) : parsed[vAxis], i); + + properties.skip = isNaN(iPixel) || isNaN(vPixel) || nullData; + properties.stop = i > 0 && (Math.abs(parsed[iAxis] - prevParsed[iAxis])) > maxGapLength; + if (segment) { + properties.parsed = parsed; + properties.raw = _dataset.data[i]; + } + + if (includeOptions) { + properties.options = sharedOptions || this.resolveDataElementOptions(i, point.active ? 'active' : mode); + } + + if (!directUpdate) { + this.updateElement(point, i, properties, mode); + } + + prevParsed = parsed; + } + + this.updateSharedOptions(sharedOptions, mode, firstOpts); + } + + /** + * @protected + */ + getMaxOverflow() { + const meta = this._cachedMeta; + const data = meta.data || []; + + if (!this.options.showLine) { + let max = 0; + for (let i = data.length - 1; i >= 0; --i) { + max = Math.max(max, data[i].size(this.resolveDataElementOptions(i)) / 2); + } + return max > 0 && max; + } + + const dataset = meta.dataset; + const border = dataset.options && dataset.options.borderWidth || 0; + + if (!data.length) { + return border; + } + + const firstPoint = data[0].size(this.resolveDataElementOptions(0)); + const lastPoint = data[data.length - 1].size(this.resolveDataElementOptions(data.length - 1)); + return Math.max(border, firstPoint, lastPoint) / 2; + } } ScatterController.id = 'scatter'; @@ -10,6 +128,8 @@ ScatterController.id = 'scatter'; * @type {any} */ ScatterController.defaults = { + datasetElementType: false, + dataElementType: 'point', showLine: false, fill: false }; diff --git a/src/helpers/helpers.extras.js b/src/helpers/helpers.extras.js index 2b73328f9ac..8bab58ae182 100644 --- a/src/helpers/helpers.extras.js +++ b/src/helpers/helpers.extras.js @@ -1,3 +1,5 @@ +import {_limitValue} from './helpers.math'; +import {_lookupByKey} from './helpers.collection'; export function fontString(pixelSize, fontStyle, fontFamily) { return fontStyle + ' ' + pixelSize + 'px ' + fontFamily; @@ -87,3 +89,68 @@ export const _textX = (align, left, right, rtl) => { const check = rtl ? 'left' : 'right'; return align === check ? right : align === 'center' ? (left + right) / 2 : left; }; + +/** + * Return start and count of visible points. + * @param {object} meta - dataset meta. + * @param {array} points - array of point elements. + * @param {boolean} animationsDisabled - if true animation is disabled. + * @returns {{start: number; count: number}} + * @private + */ +export function _getStartAndCountOfVisiblePoints(meta, points, animationsDisabled) { + const pointCount = points.length; + + let start = 0; + let count = pointCount; + + if (meta._sorted) { + const {iScale, _parsed} = meta; + const axis = iScale.axis; + const {min, max, minDefined, maxDefined} = iScale.getUserBounds(); + + if (minDefined) { + start = _limitValue(Math.min( + _lookupByKey(_parsed, iScale.axis, min).lo, + animationsDisabled ? pointCount : _lookupByKey(points, axis, iScale.getPixelForValue(min)).lo), + 0, pointCount - 1); + } + if (maxDefined) { + count = _limitValue(Math.max( + _lookupByKey(_parsed, iScale.axis, max, true).hi + 1, + animationsDisabled ? 0 : _lookupByKey(points, axis, iScale.getPixelForValue(max), true).hi + 1), + start, pointCount) - start; + } else { + count = pointCount - start; + } + } + + return {start, count}; +} + +/** + * Checks if the scale ranges have changed. + * @param {object} meta - dataset meta. + * @returns {boolean} + * @private + */ +export function _scaleRangesChanged(meta) { + const {xScale, yScale, _scaleRanges} = meta; + const newRanges = { + xmin: xScale.min, + xmax: xScale.max, + ymin: yScale.min, + ymax: yScale.max + }; + if (!_scaleRanges) { + meta._scaleRanges = newRanges; + return true; + } + const changed = _scaleRanges.xmin !== xScale.min + || _scaleRanges.xmax !== xScale.max + || _scaleRanges.ymin !== yScale.min + || _scaleRanges.ymax !== yScale.max; + + Object.assign(_scaleRanges, newRanges); + return changed; +} diff --git a/test/specs/controller.scatter.tests.js b/test/specs/controller.scatter.tests.js index 3f0b17594d5..1e89849a40f 100644 --- a/test/specs/controller.scatter.tests.js +++ b/test/specs/controller.scatter.tests.js @@ -61,4 +61,107 @@ describe('Chart.controllers.scatter', function() { await jasmine.triggerMouseEvent(chart, 'mousemove', point); expect(chart.tooltip.body.length).toEqual(1); }); + + it('should not create line element by default', function() { + var chart = window.acquireChart({ + type: 'scatter', + data: { + datasets: [{ + data: [{ + x: 10, + y: 15 + }, + { + x: 12, + y: 10 + }], + label: 'dataset1' + }, + { + data: [{ + x: 20, + y: 10 + }, + { + x: 4, + y: 8 + }], + label: 'dataset2' + }] + }, + }); + + var meta = chart.getDatasetMeta(0); + expect(meta.dataset instanceof Chart.elements.LineElement).toBe(false); + }); + + it('should create line element if showline is true at datasets options', function() { + var chart = window.acquireChart({ + type: 'scatter', + data: { + datasets: [{ + showLine: true, + data: [{ + x: 10, + y: 15 + }, + { + x: 12, + y: 10 + }], + label: 'dataset1' + }, + { + data: [{ + x: 20, + y: 10 + }, + { + x: 4, + y: 8 + }], + label: 'dataset2' + }] + }, + }); + + var meta = chart.getDatasetMeta(0); + expect(meta.dataset instanceof Chart.elements.LineElement).toBe(true); + }); + + it('should create line element if showline is true at root options', function() { + var chart = window.acquireChart({ + type: 'scatter', + data: { + datasets: [{ + data: [{ + x: 10, + y: 15 + }, + { + x: 12, + y: 10 + }], + label: 'dataset1' + }, + { + data: [{ + x: 20, + y: 10 + }, + { + x: 4, + y: 8 + }], + label: 'dataset2' + }] + }, + options: { + showLine: true + } + }); + + var meta = chart.getDatasetMeta(0); + expect(meta.dataset instanceof Chart.elements.LineElement).toBe(true); + }); });