UNPKG

devextreme

Version:

HTML5 JavaScript Component Suite for Responsive Web Development

734 lines (726 loc) • 29.3 kB
/** * DevExtreme (esm/viz/series/scatter_series.js) * Version: 24.2.6 * Build date: Mon Mar 17 2025 * * Copyright (c) 2012 - 2025 Developer Express Inc. ALL RIGHTS RESERVED * Read about DevExtreme licensing here: https://js.devexpress.com/Licensing/ */ import { extend as _extend } from "../../core/utils/extend"; import { each as _each } from "../../core/utils/iterator"; import rangeCalculator from "./helpers/range_data_calculator"; import { isDefined as _isDefined, isString as _isString } from "../../core/utils/type"; import { map as _map, normalizeEnum as _normalizeEnum, convertXYToPolar, extractColor } from "../core/utils"; import { noop as _noop } from "../../core/utils/common"; const math = Math; const _abs = math.abs; const _sqrt = math.sqrt; const _max = math.max; const DEFAULT_TRACKER_WIDTH = 12; const DEFAULT_DURATION = 400; const HIGH_ERROR = "highError"; const LOW_ERROR = "lowError"; const VARIANCE = "variance"; const STANDARD_DEVIATION = "stddeviation"; const STANDARD_ERROR = "stderror"; const PERCENT = "percent"; const FIXED = "fixed"; const UNDEFINED = "undefined"; const DISCRETE = "discrete"; const LOGARITHMIC = "logarithmic"; const DATETIME = "datetime"; let chart = {}; let polar = {}; function sum(array) { let result = 0; _each(array, (function(_, value) { result += value })); return result } function isErrorBarTypeCorrect(type) { return [FIXED, PERCENT, VARIANCE, STANDARD_DEVIATION, STANDARD_ERROR].includes(type) } function variance(array, expectedValue) { return sum(_map(array, (function(value) { return (value - expectedValue) * (value - expectedValue) }))) / array.length } function calculateAvgErrorBars(result, data, series) { const errorBarsOptions = series.getOptions().valueErrorBar; const valueField = series.getValueFields()[0]; const lowValueField = errorBarsOptions.lowValueField || LOW_ERROR; const highValueField = errorBarsOptions.highValueField || HIGH_ERROR; if (series.areErrorBarsVisible() && void 0 === errorBarsOptions.type) { const fusionData = data.reduce((function(result, item) { if (_isDefined(item[lowValueField])) { result[0] += item[valueField] - item[lowValueField]; result[1]++ } if (_isDefined(item[highValueField])) { result[2] += item[highValueField] - item[valueField]; result[3]++ } return result }), [0, 0, 0, 0]); if (fusionData[1]) { result[lowValueField] = result[valueField] - fusionData[0] / fusionData[1] } if (fusionData[2]) { result[highValueField] = result[valueField] + fusionData[2] / fusionData[3] } } return result } function calculateSumErrorBars(result, data, series) { const errorBarsOptions = series.getOptions().valueErrorBar; const lowValueField = errorBarsOptions.lowValueField || LOW_ERROR; const highValueField = errorBarsOptions.highValueField || HIGH_ERROR; if (series.areErrorBarsVisible() && void 0 === errorBarsOptions.type) { result[lowValueField] = 0; result[highValueField] = 0; result = data.reduce((function(result, item) { result[lowValueField] += item[lowValueField]; result[highValueField] += item[highValueField]; return result }), result) } return result } function getMinMaxAggregator(compare) { return (_ref, series) => { let { intervalStart: intervalStart, intervalEnd: intervalEnd, data: data } = _ref; const valueField = series.getValueFields()[0]; let targetData = data[0]; targetData = data.reduce(((result, item) => { const value = item[valueField]; if (null === result[valueField]) { result = item } if (null !== value && compare(value, result[valueField])) { return item } return result }), targetData); return _extend({}, targetData, { [series.getArgumentField()]: series._getIntervalCenter(intervalStart, intervalEnd) }) } } function checkFields(data, fieldsToCheck, skippedFields) { let allFieldsIsValid = true; for (const field in fieldsToCheck) { const isArgument = "argument" === field; if (isArgument || "size" === field ? !_isDefined(data[field]) : void 0 === data[field]) { const selector = fieldsToCheck[field]; if (!isArgument) { skippedFields[selector] = (skippedFields[selector] || 0) + 1 } allFieldsIsValid = false } } return allFieldsIsValid } const baseScatterMethods = { _defaultDuration: 400, _defaultTrackerWidth: 12, _applyStyle: _noop, _updateOptions: _noop, _parseStyle: _noop, _prepareSegment: _noop, _drawSegment: _noop, _appendInGroup: function() { this._group.append(this._extGroups.seriesGroup) }, _createLegendState: function(styleOptions, defaultColor) { return { fill: extractColor(styleOptions.color, true) || defaultColor, hatching: styleOptions.hatching ? _extend({}, styleOptions.hatching, { direction: "right" }) : void 0 } }, _getColorId: _noop, _applyElementsClipRect: function(settings) { settings["clip-path"] = this._paneClipRectID }, _applyMarkerClipRect: function(settings) { settings["clip-path"] = this._forceClipping ? this._paneClipRectID : null }, _createGroup: function(groupName, parent, target, settings) { const group = parent[groupName] = parent[groupName] || this._renderer.g(); target && group.append(target); settings && group.attr(settings) }, _applyClearingSettings: function(settings) { settings.opacity = null; settings.scale = null; if (this._options.rotated) { settings.translateX = null } else { settings.translateY = null } }, _createGroups: function() { this._createGroup("_markersGroup", this, this._group); this._createGroup("_labelsGroup", this) }, _setMarkerGroupSettings: function() { const settings = this._createPointStyles(this._getMarkerGroupOptions()).normal; settings.class = "dxc-markers"; settings.opacity = 1; this._applyMarkerClipRect(settings); this._markersGroup.attr(settings) }, getVisibleArea: function() { return this._visibleArea }, areErrorBarsVisible: function() { const errorBarOptions = this._options.valueErrorBar; return errorBarOptions && this._errorBarsEnabled() && "none" !== errorBarOptions.displayMode && (isErrorBarTypeCorrect(_normalizeEnum(errorBarOptions.type)) || _isDefined(errorBarOptions.lowValueField) || _isDefined(errorBarOptions.highValueField)) }, groupPointsByCoords(rotated) { const cat = []; _each(this.getVisiblePoints(), (function(_, p) { const pointCoord = parseInt(rotated ? p.vy : p.vx); if (!cat[pointCoord]) { cat[pointCoord] = p } else { Array.isArray(cat[pointCoord]) ? cat[pointCoord].push(p) : cat[pointCoord] = [cat[pointCoord], p] } })); return cat }, _createErrorBarGroup: function(animationEnabled) { const that = this; const errorBarOptions = that._options.valueErrorBar; let settings; if (that.areErrorBarsVisible()) { settings = { class: "dxc-error-bars", stroke: errorBarOptions.color, "stroke-width": errorBarOptions.lineWidth, opacity: animationEnabled ? .001 : errorBarOptions.opacity || 1, "stroke-linecap": "square", sharp: true, "clip-path": that._forceClipping ? that._paneClipRectID : that._widePaneClipRectID }; that._createGroup("_errorBarGroup", that, that._group, settings) } }, _setGroupsSettings: function(animationEnabled) { this._setMarkerGroupSettings(); this._setLabelGroupSettings(animationEnabled); this._createErrorBarGroup(animationEnabled) }, _getCreatingPointOptions: function() { const that = this; let defaultPointOptions; let creatingPointOptions = that._predefinedPointOptions; let normalStyle; if (!creatingPointOptions) { defaultPointOptions = that._getPointOptions(); that._predefinedPointOptions = creatingPointOptions = _extend(true, { styles: {} }, defaultPointOptions); normalStyle = defaultPointOptions.styles && defaultPointOptions.styles.normal || {}; creatingPointOptions.styles = creatingPointOptions.styles || {}; creatingPointOptions.styles.normal = { "stroke-width": normalStyle["stroke-width"], r: normalStyle.r, opacity: normalStyle.opacity } } return creatingPointOptions }, _getPointOptions: function() { return this._parsePointOptions(this._preparePointOptions(), this._options.label) }, _getOptionsForPoint: function() { return this._options.point }, _parsePointStyle: function(style, defaultColor, defaultBorderColor, defaultSize) { const border = style.border || {}; const sizeValue = void 0 !== style.size ? style.size : defaultSize; return { fill: extractColor(style.color, true) || defaultColor, stroke: border.color || defaultBorderColor, "stroke-width": border.visible ? border.width : 0, r: sizeValue / 2 + (border.visible && 0 !== sizeValue ? ~~(border.width / 2) || 0 : 0) } }, _createPointStyles: function(pointOptions) { const mainPointColor = extractColor(pointOptions.color, true) || this._options.mainSeriesColor; const containerColor = this._options.containerBackgroundColor; const normalStyle = this._parsePointStyle(pointOptions, mainPointColor, mainPointColor); normalStyle.visibility = pointOptions.visible ? "visible" : "hidden"; return { labelColor: mainPointColor, normal: normalStyle, hover: this._parsePointStyle(pointOptions.hoverStyle, containerColor, mainPointColor, pointOptions.size), selection: this._parsePointStyle(pointOptions.selectionStyle, containerColor, mainPointColor, pointOptions.size) } }, _checkData: function(data, skippedFields, fieldsToCheck) { fieldsToCheck = fieldsToCheck || { value: this.getValueFields()[0] }; fieldsToCheck.argument = this.getArgumentField(); return checkFields(data, fieldsToCheck, skippedFields || {}) && data.value === data.value }, getArgumentRangeInitialValue() { const points = this.getPoints(); if (this.useAggregation() && points.length) { var _points$0$aggregation, _points$aggregationIn; return { min: null === (_points$0$aggregation = points[0].aggregationInfo) || void 0 === _points$0$aggregation ? void 0 : _points$0$aggregation.intervalStart, max: null === (_points$aggregationIn = points[points.length - 1].aggregationInfo) || void 0 === _points$aggregationIn ? void 0 : _points$aggregationIn.intervalEnd } } return }, getValueRangeInitialValue: function() { return }, _getRangeData: function() { return rangeCalculator.getRangeData(this) }, _getPointDataSelector: function() { const valueField = this.getValueFields()[0]; const argumentField = this.getArgumentField(); const tagField = this.getTagField(); const areErrorBarsVisible = this.areErrorBarsVisible(); let lowValueField; let highValueField; if (areErrorBarsVisible) { const errorBarOptions = this._options.valueErrorBar; lowValueField = errorBarOptions.lowValueField || LOW_ERROR; highValueField = errorBarOptions.highValueField || HIGH_ERROR } return data => { const pointData = { value: this._processEmptyValue(data[valueField]), argument: data[argumentField], tag: data[tagField], data: data }; if (areErrorBarsVisible) { pointData.lowError = data[lowValueField]; pointData.highError = data[highValueField] } return pointData } }, _errorBarsEnabled: function() { return this.valueAxisType !== DISCRETE && this.valueAxisType !== LOGARITHMIC && this.valueType !== DATETIME }, _drawPoint: function(options) { const point = options.point; if (point.isInVisibleArea()) { point.clearVisibility(); point.draw(this._renderer, options.groups, options.hasAnimation, options.firstDrawing); this._drawnPoints.push(point) } else { point.setInvisibility() } }, _animateComplete: function() { const animationSettings = { duration: this._defaultDuration }; this._labelsGroup && this._labelsGroup.animate({ opacity: 1 }, animationSettings); this._errorBarGroup && this._errorBarGroup.animate({ opacity: this._options.valueErrorBar.opacity || 1 }, animationSettings) }, _animate: function() { const that = this; const lastPointIndex = that._drawnPoints.length - 1; _each(that._drawnPoints || [], (function(i, p) { p.animate(i === lastPointIndex ? function() { that._animateComplete() } : void 0, { translateX: p.x, translateY: p.y }) })) }, _getIntervalCenter(intervalStart, intervalEnd) { const argAxis = this.getArgumentAxis(); const axisOptions = argAxis.getOptions(); if (argAxis.aggregatedPointBetweenTicks()) { return intervalStart } return "discrete" !== axisOptions.type ? argAxis.getVisualRangeCenter({ minVisible: intervalStart, maxVisible: intervalEnd }, true) : intervalStart }, _defaultAggregator: "avg", _aggregators: { avg(_ref2, series) { let { data: data, intervalStart: intervalStart, intervalEnd: intervalEnd } = _ref2; if (!data.length) { return } const valueField = series.getValueFields()[0]; const aggregationResult = data.reduce(((result, item) => { const value = item[valueField]; if (_isDefined(value)) { result[0] += value; result[1]++ } else if (null === value) { result[2]++ } return result }), [0, 0, 0]); return calculateAvgErrorBars({ [valueField]: aggregationResult[2] === data.length ? null : aggregationResult[0] / aggregationResult[1], [series.getArgumentField()]: series._getIntervalCenter(intervalStart, intervalEnd) }, data, series) }, sum(_ref3, series) { let { intervalStart: intervalStart, intervalEnd: intervalEnd, data: data } = _ref3; if (!data.length) { return } const valueField = series.getValueFields()[0]; const aggregationResult = data.reduce(((result, item) => { const value = item[valueField]; if (void 0 !== value) { result[0] += value } if (null === value) { result[1]++ } else if (void 0 === value) { result[2]++ } return result }), [0, 0, 0]); let value = aggregationResult[0]; if (aggregationResult[1] === data.length) { value = null } if (aggregationResult[2] === data.length) { return } return calculateSumErrorBars({ [valueField]: value, [series.getArgumentField()]: series._getIntervalCenter(intervalStart, intervalEnd) }, data, series) }, count(_ref4, series) { let { data: data, intervalStart: intervalStart, intervalEnd: intervalEnd } = _ref4; const valueField = series.getValueFields()[0]; return { [series.getArgumentField()]: series._getIntervalCenter(intervalStart, intervalEnd), [valueField]: data.filter((i => void 0 !== i[valueField])).length } }, min: getMinMaxAggregator(((a, b) => a < b)), max: getMinMaxAggregator(((a, b) => a > b)) }, _endUpdateData: function() { delete this._predefinedPointOptions }, getArgumentField: function() { return this._options.argumentField || "arg" }, getValueFields: function() { const options = this._options; const errorBarsOptions = options.valueErrorBar; const valueFields = [options.valueField || "val"]; let lowValueField; let highValueField; if (errorBarsOptions) { lowValueField = errorBarsOptions.lowValueField; highValueField = errorBarsOptions.highValueField; _isString(lowValueField) && valueFields.push(lowValueField); _isString(highValueField) && valueFields.push(highValueField) } return valueFields }, _calculateErrorBars: function(data) { if (!this.areErrorBarsVisible()) { return } const options = this._options; const errorBarsOptions = options.valueErrorBar; const errorBarType = _normalizeEnum(errorBarsOptions.type); let floatErrorValue = parseFloat(errorBarsOptions.value); const valueField = this.getValueFields()[0]; let value; const lowValueField = errorBarsOptions.lowValueField || LOW_ERROR; const highValueField = errorBarsOptions.highValueField || HIGH_ERROR; let valueArray; let valueArrayLength; let meanValue; let processDataItem; const addSubError = function(_i, item) { value = item.value; item.lowError = value - floatErrorValue; item.highError = value + floatErrorValue }; switch (errorBarType) { case FIXED: processDataItem = addSubError; break; case PERCENT: processDataItem = function(_, item) { value = item.value; const error = value * floatErrorValue / 100; item.lowError = value - error; item.highError = value + error }; break; case UNDEFINED: processDataItem = function(_, item) { item.lowError = item.data[lowValueField]; item.highError = item.data[highValueField] }; break; default: valueArray = _map(data, (function(item) { return _isDefined(item.data[valueField]) ? item.data[valueField] : null })); valueArrayLength = valueArray.length; floatErrorValue = floatErrorValue || 1; switch (errorBarType) { case VARIANCE: floatErrorValue = variance(valueArray, sum(valueArray) / valueArrayLength) * floatErrorValue; processDataItem = addSubError; break; case STANDARD_DEVIATION: meanValue = sum(valueArray) / valueArrayLength; floatErrorValue = _sqrt(variance(valueArray, meanValue)) * floatErrorValue; processDataItem = function(_, item) { item.lowError = meanValue - floatErrorValue; item.highError = meanValue + floatErrorValue }; break; case STANDARD_ERROR: floatErrorValue = _sqrt(variance(valueArray, sum(valueArray) / valueArrayLength) / valueArrayLength) * floatErrorValue; processDataItem = addSubError } } processDataItem && _each(data, processDataItem) }, _patchMarginOptions: function(options) { const pointOptions = this._getCreatingPointOptions(); const styles = pointOptions.styles; const maxSize = [styles.normal, styles.hover, styles.selection].reduce((function(max, style) { return _max(max, 2 * style.r + style["stroke-width"]) }), 0); options.size = pointOptions.visible ? maxSize : 0; options.sizePointNormalState = pointOptions.visible ? 2 * styles.normal.r + styles.normal["stroke-width"] : 2; return options }, usePointsToDefineAutoHiding() { return !!this._getOptionsForPoint().visible } }; chart = _extend({}, baseScatterMethods, { drawTrackers: function() { const that = this; let trackers; let trackersGroup; const segments = that._segments || []; const rotated = that._options.rotated; if (!that.isVisible()) { return } if (segments.length) { trackers = that._trackers = that._trackers || []; trackersGroup = that._trackersGroup = (that._trackersGroup || that._renderer.g().attr({ fill: "gray", opacity: .001, stroke: "gray", class: "dxc-trackers" })).attr({ "clip-path": this._paneClipRectID || null }).append(that._group); _each(segments, (function(i, segment) { if (!trackers[i]) { trackers[i] = that._drawTrackerElement(segment).data({ "chart-data-series": that }).append(trackersGroup) } else { that._updateTrackerElement(segment, trackers[i]) } })) } that._trackersTranslator = that.groupPointsByCoords(rotated) }, _checkAxisVisibleAreaCoord(isArgument, coord) { const axis = isArgument ? this.getArgumentAxis() : this.getValueAxis(); const visibleArea = axis.getVisibleArea(); return _isDefined(coord) && visibleArea[0] <= coord && visibleArea[1] >= coord }, checkSeriesViewportCoord(axis, coord) { return this.getPoints().length && this.isVisible() }, getSeriesPairCoord(coord, isArgument) { let oppositeCoord = null; const isOpposite = !isArgument && !this._options.rotated || isArgument && this._options.rotated; const coordName = !isOpposite ? "vx" : "vy"; const oppositeCoordName = !isOpposite ? "vy" : "vx"; const points = this.getVisiblePoints(); for (let i = 0; i < points.length; i++) { const p = points[i]; const tmpCoord = p[coordName] === coord ? p[oppositeCoordName] : void 0; if (this._checkAxisVisibleAreaCoord(!isArgument, tmpCoord)) { oppositeCoord = tmpCoord; break } } return oppositeCoord }, _getNearestPoints: (point, nextPoint) => [point, nextPoint], _getBezierPoints: () => [], _getNearestPointsByCoord(coord, isArgument) { const that = this; const rotated = that.getOptions().rotated; const isOpposite = !isArgument && !rotated || isArgument && rotated; const coordName = isOpposite ? "vy" : "vx"; const allPoints = that.getPoints(); const bezierPoints = that._getBezierPoints(); const nearestPoints = []; if (allPoints.length > 1) { allPoints.forEach(((point, i) => { const nextPoint = allPoints[i + 1]; if (nextPoint && (point[coordName] <= coord && nextPoint[coordName] >= coord || point[coordName] >= coord && nextPoint[coordName] <= coord)) { nearestPoints.push(that._getNearestPoints(point, nextPoint, bezierPoints)) } })) } else { nearestPoints.push([allPoints[0], allPoints[0]]) } return nearestPoints }, getNeighborPoint: function(x, y) { let pCoord = this._options.rotated ? y : x; let nCoord = pCoord; const cat = this._trackersTranslator; let point = null; let minDistance; const oppositeCoord = this._options.rotated ? x : y; const oppositeCoordName = this._options.rotated ? "vx" : "vy"; if (this.isVisible() && cat) { point = cat[pCoord]; do { point = cat[nCoord] || cat[pCoord]; pCoord--; nCoord++ } while ((pCoord >= 0 || nCoord < cat.length) && !point); if (Array.isArray(point)) { minDistance = _abs(point[0][oppositeCoordName] - oppositeCoord); _each(point, (function(i, p) { const distance = _abs(p[oppositeCoordName] - oppositeCoord); if (minDistance >= distance) { minDistance = distance; point = p } })) } } return point }, _applyVisibleArea: function() { const rotated = this._options.rotated; const visibleX = (rotated ? this.getValueAxis() : this.getArgumentAxis()).getVisibleArea(); const visibleY = (rotated ? this.getArgumentAxis() : this.getValueAxis()).getVisibleArea(); this._visibleArea = { minX: visibleX[0], maxX: visibleX[1], minY: visibleY[0], maxY: visibleY[1] } }, getPointCenterByArg(arg) { const point = this.getPointsByArg(arg)[0]; return point ? point.getCenterCoord() : void 0 } }); polar = _extend({}, baseScatterMethods, { drawTrackers: function() { chart.drawTrackers.call(this); const cat = this._trackersTranslator; let index; if (!this.isVisible()) { return } _each(cat, (function(i, category) { if (category) { index = i; return false } })); cat[index + 360] = cat[index] }, getNeighborPoint: function(x, y) { const pos = convertXYToPolar(this.getValueAxis().getCenter(), x, y); return chart.getNeighborPoint.call(this, pos.phi, pos.r) }, _applyVisibleArea: function() { const canvas = this.getValueAxis().getCanvas(); this._visibleArea = { minX: canvas.left, maxX: canvas.width - canvas.right, minY: canvas.top, maxY: canvas.height - canvas.bottom } }, getSeriesPairCoord(params, isArgument) { let coords = null; const paramName = isArgument ? "argument" : "radius"; const points = this.getVisiblePoints(); for (let i = 0; i < points.length; i++) { const p = points[i]; const tmpPoint = _isDefined(p[paramName]) && _isDefined(params[paramName]) && p[paramName].valueOf() === params[paramName].valueOf() ? { x: p.x, y: p.y } : void 0; if (_isDefined(tmpPoint)) { coords = tmpPoint; break } } return coords } }); export { chart, polar };