devextreme
Version:
HTML5 JavaScript Component Suite for Responsive Web Development
734 lines (726 loc) • 29.3 kB
JavaScript
/**
* 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
};