devextreme
Version:
HTML5 JavaScript Component Suite for Responsive Web Development
691 lines (688 loc) • 25.9 kB
JavaScript
/**
* DevExtreme (cjs/viz/axes/polar_axes.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/
*/
"use strict";
exports.linearSpider = exports.linear = exports.circularSpider = exports.circular = void 0;
var _utils = require("../core/utils");
var _type = require("../../core/utils/type");
var _extend = require("../../core/utils/extend");
var _axes_constants = _interopRequireDefault(require("./axes_constants"));
var _xy_axes = _interopRequireDefault(require("./xy_axes"));
var _tick = require("./tick");
var _axes_utils = require("./axes_utils");
var _common = require("../../core/utils/common");
function _interopRequireDefault(e) {
return e && e.__esModule ? e : {
default: e
}
}
const {
PI: PI,
abs: abs,
atan: atan,
round: round
} = Math;
const _min = Math.min;
const _max = Math.max;
const xyAxesLinear = _xy_axes.default.linear;
const HALF_PI_ANGLE = 90;
function getPolarQuarter(angle) {
let quarter;
angle = (0, _utils.normalizeAngle)(angle);
if (angle >= 315 && angle <= 360 || angle < 45 && angle >= 0) {
quarter = 1
} else if (angle >= 45 && angle < 135) {
quarter = 2
} else if (angle >= 135 && angle < 225) {
quarter = 3
} else if (angle >= 225 && angle < 315) {
quarter = 4
}
return quarter
}
const circularAxes = {
_calculateValueMargins(ticks) {
let {
minVisible: minVisible,
maxVisible: maxVisible
} = this._getViewportRange();
if (ticks && ticks.length > 1) {
minVisible = minVisible < ticks[0].value ? minVisible : ticks[0].value;
maxVisible = minVisible > ticks[ticks.length - 1].value ? maxVisible : ticks[ticks.length - 1].value
}
return {
minValue: minVisible,
maxValue: maxVisible
}
},
applyMargins() {
const margins = this._calculateValueMargins(this._majorTicks);
const br = this._translator.getBusinessRange();
br.addRange({
minVisible: margins.minValue,
maxVisible: margins.maxValue,
interval: this._calculateRangeInterval(br.interval)
});
this._translator.updateBusinessRange(br)
},
_getTranslatorOptions: function() {
return {
isHorizontal: true,
conversionValue: true,
addSpiderCategory: this._getSpiderCategoryOption(),
stick: this._getStick()
}
},
getCenter: function() {
return this._center
},
getRadius: function() {
return this._radius
},
getAngles: function() {
const options = this._options;
return [options.startAngle, options.endAngle]
},
_updateRadius(canvas) {
const rad = _min(canvas.width - canvas.left - canvas.right, canvas.height - canvas.top - canvas.bottom) / 2;
this._radius = rad < 0 ? 0 : rad
},
_updateCenter: function(canvas) {
this._center = {
x: canvas.left + (canvas.width - canvas.right - canvas.left) / 2,
y: canvas.top + (canvas.height - canvas.top - canvas.bottom) / 2
}
},
_processCanvas: function(canvas) {
this._updateRadius(canvas);
this._updateCenter(canvas);
return {
left: 0,
right: 0,
width: this._getScreenDelta()
}
},
_createAxisElement: function() {
return this._renderer.circle()
},
_updateAxisElementPosition: function() {
const center = this.getCenter();
this._axisElement.attr({
cx: center.x,
cy: center.y,
r: this.getRadius()
})
},
_boundaryTicksVisibility: {
min: true
},
_getSpiderCategoryOption: function() {
return this._options.firstPointOnStartAngle
},
_validateOptions(options) {
const that = this;
let originValue = options.originValue;
const wholeRange = options.wholeRange = {};
const period = options.period;
if ((0, _type.isDefined)(originValue)) {
originValue = that.validateUnit(originValue)
}
if (period > 0 && options.argumentType === _axes_constants.default.numeric) {
originValue = originValue || 0;
wholeRange.endValue = originValue + period;
that._viewport = (0, _utils.getVizRangeObject)([originValue, wholeRange.endValue])
}
if ((0, _type.isDefined)(originValue)) {
wholeRange.startValue = originValue
}
},
getMargins() {
const tickOptions = this._options.tick;
const tickOuterLength = _max(tickOptions.visible ? tickOptions.length / 2 + tickOptions.shift : 0, 0);
const radius = this.getRadius();
const {
x: x,
y: y
} = this._center;
const labelBoxes = this._majorTicks.map((t => t.label && t.label.getBBox())).filter((b => b));
const canvas = (0, _extend.extend)({}, this._canvas, {
left: x - radius,
top: y - radius,
right: this._canvas.width - (x + radius),
bottom: this._canvas.height - (y + radius)
});
const margins = (0, _axes_utils.calculateCanvasMargins)(labelBoxes, canvas);
Object.keys(margins).forEach((k => margins[k] = margins[k] < tickOuterLength ? tickOuterLength : margins[k]));
return margins
},
_updateLabelsPosition() {
(0, _axes_utils.measureLabels)(this._majorTicks);
this._adjustLabelsCoord(0, 0, true);
this._checkBoundedLabelsOverlapping(this._majorTicks, this._majorTicks.map((t => t.labelBBox)))
},
_setVisualRange: _common.noop,
applyVisualRangeSetter: _common.noop,
_getStick: function() {
return this._options.firstPointOnStartAngle || this._options.type !== _axes_constants.default.discrete
},
_getTranslatedCoord: function(value, offset) {
return this._translator.translate(value, offset) - 90
},
_getCanvasStartEnd: function() {
return {
start: -90,
end: 270
}
},
_getStripGraphicAttributes: function(fromAngle, toAngle) {
const center = this.getCenter();
const angle = this.getAngles()[0];
const r = this.getRadius();
return {
x: center.x,
y: center.y,
innerRadius: 0,
outerRadius: r,
startAngle: -toAngle - angle,
endAngle: -fromAngle - angle
}
},
_createStrip: function(coords) {
return this._renderer.arc(coords.x, coords.y, coords.innerRadius, coords.outerRadius, coords.startAngle, coords.endAngle)
},
_getStripLabelCoords: function(from, to) {
const coords = this._getStripGraphicAttributes(from, to);
const angle = coords.startAngle + (coords.endAngle - coords.startAngle) / 2;
const cosSin = (0, _utils.getCosAndSin)(angle);
const halfRad = this.getRadius() / 2;
const center = this.getCenter();
const x = round(center.x + halfRad * cosSin.cos);
const y = round(center.y - halfRad * cosSin.sin);
return {
x: x,
y: y,
align: _axes_constants.default.center
}
},
_getConstantLineGraphicAttributes: function(value) {
const center = this.getCenter();
const r = this.getRadius();
return {
points: [center.x, center.y, center.x + r, center.y]
}
},
_createConstantLine: function(value, attr) {
return this._createPathElement(this._getConstantLineGraphicAttributes(value).points, attr)
},
_rotateConstantLine(line, value) {
const {
x: x,
y: y
} = this.getCenter();
line.rotate(value + this.getAngles()[0], x, y)
},
_getConstantLineLabelsCoords: function(value) {
const cosSin = (0, _utils.getCosAndSin)(-value - this.getAngles()[0]);
const halfRad = this.getRadius() / 2;
const center = this.getCenter();
const x = round(center.x + halfRad * cosSin.cos);
const y = round(center.y - halfRad * cosSin.sin);
return {
x: x,
y: y
}
},
_checkAlignmentConstantLineLabels: _common.noop,
_adjustDivisionFactor: function(val) {
return 180 * val / (this.getRadius() * PI)
},
_getScreenDelta: function() {
const angles = this.getAngles();
return abs(angles[0] - angles[1])
},
_getTickMarkPoints: function(coords, length, _ref) {
let {
shift: shift = 0
} = _ref;
const center = this.getCenter();
const radiusWithTicks = this.getRadius() + length * {
inside: -1,
center: -.5,
outside: 0
} [this._options.tickOrientation || "center"];
return [center.x + radiusWithTicks + shift, center.y, center.x + radiusWithTicks + length + shift, center.y]
},
_getLabelAdjustedCoord: function(tick, _offset, _maxWidth, checkCanvas) {
const that = this;
const labelCoords = tick.labelCoords;
const labelY = labelCoords.y;
const labelAngle = labelCoords.angle;
const cosSin = (0, _utils.getCosAndSin)(labelAngle);
const cos = cosSin.cos;
const sin = cosSin.sin;
const box = tick.labelBBox;
const halfWidth = box.width / 2;
const halfHeight = box.height / 2;
const indentFromAxis = that._options.label.indentFromAxis || 0;
const x = labelCoords.x + indentFromAxis * cos;
const y = labelY + (labelY - box.y - halfHeight) + indentFromAxis * sin;
let shiftX = 0;
let shiftY = 0;
switch (getPolarQuarter(labelAngle)) {
case 1:
shiftX = halfWidth;
shiftY = halfHeight * sin;
break;
case 2:
shiftX = halfWidth * cos;
shiftY = halfHeight;
break;
case 3:
shiftX = -halfWidth;
shiftY = halfHeight * sin;
break;
case 4:
shiftX = halfWidth * cos;
shiftY = -halfHeight
}
if (checkCanvas) {
const canvas = that._canvas;
const boxShiftX = x - labelCoords.x + shiftX;
const boxShiftY = y - labelCoords.y + shiftY;
if (box.x + boxShiftX < canvas.originalLeft) {
shiftX -= box.x + boxShiftX - canvas.originalLeft
}
if (box.x + box.width + boxShiftX > canvas.width - canvas.originalRight) {
shiftX -= box.x + box.width + boxShiftX - (canvas.width - canvas.originalRight)
}
if (box.y + boxShiftY < canvas.originalTop) {
shiftY -= box.y + boxShiftY - canvas.originalTop
}
if (box.y + box.height + boxShiftY > canvas.height - canvas.originalBottom) {
shiftY -= box.y + box.height + boxShiftY - (canvas.height - canvas.originalBottom)
}
}
return {
x: x + shiftX,
y: y + shiftY
}
},
_getGridLineDrawer: function() {
const that = this;
return function(tick, gridStyle) {
const center = that.getCenter();
return that._createPathElement(that._getGridPoints().points, gridStyle).rotate(tick.coords.angle, center.x, center.y)
}
},
_getGridPoints: function() {
const r = this.getRadius();
const center = this.getCenter();
return {
points: [center.x, center.y, center.x + r, center.y]
}
},
_getTranslatedValue: function(value, offset) {
const startAngle = this.getAngles()[0];
const angle = this._translator.translate(value, -offset);
const coords = (0, _utils.convertPolarToXY)(this.getCenter(), startAngle, angle, this.getRadius());
return {
x: coords.x,
y: coords.y,
angle: this.getTranslatedAngle(angle)
}
},
_getAdjustedStripLabelCoords: function(strip) {
const box = strip.labelBBox;
return {
translateY: strip.label.attr("y") - box.y - box.height / 2
}
},
coordsIn: function(x, y) {
return (0, _utils.convertXYToPolar)(this.getCenter(), x, y).r > this.getRadius()
},
_rotateTick: function(element, coords) {
const center = this.getCenter();
element.rotate(coords.angle, center.x, center.y)
},
_validateOverlappingMode: function(mode) {
return _axes_constants.default.validateOverlappingMode(mode)
},
_validateDisplayMode: function() {
return "standard"
},
_getStep: function(boxes) {
const radius = this.getRadius() + (this._options.label.indentFromAxis || 0);
const maxLabelBox = boxes.reduce((function(prevValue, box) {
const curValue = prevValue;
if (prevValue.width < box.width) {
curValue.width = box.width
}
if (prevValue.height < box.height) {
curValue.height = box.height
}
return curValue
}), {
width: 0,
height: 0
});
const angle1 = abs(2 * atan(maxLabelBox.height / (2 * radius - maxLabelBox.width)) * 180 / PI);
const angle2 = abs(2 * atan(maxLabelBox.width / (2 * radius - maxLabelBox.height)) * 180 / PI);
return _axes_constants.default.getTicksCountInRange(this._majorTicks, "angle", _max(angle1, angle2))
},
_checkBoundedLabelsOverlapping: function(majorTicks, boxes, mode) {
const labelOpt = this._options.label;
mode = mode || this._validateOverlappingMode(labelOpt.overlappingBehavior);
if ("hide" !== mode) {
return
}
const lastVisibleLabelIndex = majorTicks.reduce(((lastVisibleLabelIndex, tick, index) => tick.label ? index : lastVisibleLabelIndex), null);
if (!lastVisibleLabelIndex) {
return
}
if (_axes_constants.default.areLabelsOverlap(boxes[0], boxes[lastVisibleLabelIndex], labelOpt.minSpacing, _axes_constants.default.center)) {
"first" === labelOpt.hideFirstOrLast ? majorTicks[0].removeLabel() : majorTicks[lastVisibleLabelIndex].removeLabel()
}
},
shift: function(margins) {
this._axisGroup.attr({
translateX: margins.right,
translateY: margins.bottom
});
this._axisElementsGroup.attr({
translateX: margins.right,
translateY: margins.bottom
})
},
getTranslatedAngle(angle) {
const startAngle = this.getAngles()[0];
return angle + startAngle - 90
}
};
const circular = exports.circular = circularAxes;
const circularSpider = exports.circularSpider = (0, _extend.extend)({}, circularAxes, {
_createAxisElement: function() {
return this._renderer.path([], "area")
},
_updateAxisElementPosition: function() {
this._axisElement.attr({
points: (0, _utils.map)(this.getSpiderTicks(), (function(tick) {
return {
x: tick.coords.x,
y: tick.coords.y
}
}))
})
},
_getStick: function() {
return true
},
_getSpiderCategoryOption: function() {
return true
},
getSpiderTicks: function() {
const ticks = this.getFullTicks();
this._spiderTicks = ticks.map((0, _tick.tick)(this, this.renderer, {}, {}, this._getSkippedCategory(ticks), true));
this._spiderTicks.forEach((function(tick) {
tick.initCoords()
}));
return this._spiderTicks
},
_getStripGraphicAttributes: function(fromAngle, toAngle) {
const center = this.getCenter();
const spiderTicks = this.getSpiderTicks();
let firstTick;
let lastTick;
let nextTick;
let tick;
const points = [];
let i = 0;
const len = spiderTicks.length;
while (i < len) {
tick = spiderTicks[i].coords;
if (tick.angle >= fromAngle && tick.angle <= toAngle) {
if (!firstTick) {
firstTick = (spiderTicks[i - 1] || spiderTicks[spiderTicks.length - 1]).coords;
points.push((tick.x + firstTick.x) / 2, (tick.y + firstTick.y) / 2)
}
points.push(tick.x, tick.y);
nextTick = (spiderTicks[i + 1] || spiderTicks[0]).coords;
lastTick = {
x: (tick.x + nextTick.x) / 2,
y: (tick.y + nextTick.y) / 2
}
}
i++
}
points.push(lastTick.x, lastTick.y);
points.push(center.x, center.y);
return {
points: points
}
},
_createStrip: function(_ref2) {
let {
points: points
} = _ref2;
return this._renderer.path(points, "area")
},
_getTranslatedCoord: function(value, offset) {
return this._translator.translate(value, offset) - 90
},
_setTickOffset: function() {
this._tickOffset = false
}
});
const linear = exports.linear = {
_resetMargins() {
this._reinitTranslator(this._getViewportRange())
},
_getStick: xyAxesLinear._getStick,
_getSpiderCategoryOption: _common.noop,
_getTranslatorOptions: function() {
return {
isHorizontal: true,
stick: this._getStick()
}
},
getRadius: circularAxes.getRadius,
getCenter: circularAxes.getCenter,
getAngles: circularAxes.getAngles,
_updateRadius: circularAxes._updateRadius,
_updateCenter: circularAxes._updateCenter,
_processCanvas(canvas) {
this._updateRadius(canvas);
this._updateCenter(canvas);
return {
left: 0,
right: 0,
startPadding: canvas.startPadding,
endPadding: canvas.endPadding,
width: this.getRadius()
}
},
_createAxisElement: xyAxesLinear._createAxisElement,
_updateAxisElementPosition: function() {
const centerCoord = this.getCenter();
this._axisElement.attr({
points: [centerCoord.x, centerCoord.y, centerCoord.x + this.getRadius(), centerCoord.y]
}).rotate(this.getAngles()[0] - 90, centerCoord.x, centerCoord.y)
},
_getScreenDelta: function() {
return this.getRadius()
},
_getTickMarkPoints: function(coords, length) {
return [coords.x - length / 2, coords.y, coords.x + length / 2, coords.y]
},
_getLabelAdjustedCoord: function(tick) {
const labelCoords = tick.labelCoords;
const labelY = labelCoords.y;
const cosSin = (0, _utils.getCosAndSin)(labelCoords.angle);
const indentFromAxis = this._options.label.indentFromAxis || 0;
const box = tick.labelBBox;
const x = labelCoords.x - abs(indentFromAxis * cosSin.sin) + abs(box.width / 2 * cosSin.cos) - box.width / 2;
const y = labelY + (labelY - box.y) - abs(box.height / 2 * cosSin.sin) + abs(indentFromAxis * cosSin.cos);
return {
x: x,
y: y
}
},
_getGridLineDrawer: function() {
const that = this;
return function(tick, gridStyle) {
const grid = that._getGridPoints(tick.coords);
return that._renderer.circle(grid.cx, grid.cy, grid.r).attr(gridStyle).sharp()
}
},
_getGridPoints: function(coords) {
const pos = this.getCenter();
const radius = (0, _utils.getDistance)(pos.x, pos.y, coords.x, coords.y);
if (radius > this.getRadius()) {
return {
cx: null,
cy: null,
r: null
}
}
return {
cx: pos.x,
cy: pos.y,
r: radius
}
},
_getTranslatedValue: function(value, offset) {
const startAngle = this.getAngles()[0];
const xy = (0, _utils.convertPolarToXY)(this.getCenter(), startAngle, 0, this._translator.translate(value, offset));
return {
x: xy.x,
y: xy.y,
angle: startAngle - 90
}
},
_getTranslatedCoord: function(value, offset) {
return this._translator.translate(value, offset)
},
_getCanvasStartEnd() {
const invert = this.getTranslator().getBusinessRange().invert;
const coords = [0, this.getRadius()];
invert && coords.reverse();
return {
start: coords[0],
end: coords[1]
}
},
_getStripGraphicAttributes: function(fromPoint, toPoint) {
const center = this.getCenter();
return {
x: center.x,
y: center.y,
innerRadius: fromPoint,
outerRadius: toPoint
}
},
_createStrip: function(attrs) {
return this._renderer.arc(attrs.x, attrs.y, attrs.innerRadius, attrs.outerRadius, 0, 360)
},
_getAdjustedStripLabelCoords: circularAxes._getAdjustedStripLabelCoords,
_getStripLabelCoords: function(from, to) {
const labelPos = from + (to - from) / 2;
const center = this.getCenter();
const y = round(center.y - labelPos);
return {
x: center.x,
y: y,
align: _axes_constants.default.center
}
},
_getConstantLineGraphicAttributes: function(value) {
const center = this.getCenter();
return {
cx: center.x,
cy: center.y,
r: value
}
},
_createConstantLine: function(value, attr) {
const attrs = this._getConstantLineGraphicAttributes(value);
return this._renderer.circle(attrs.cx, attrs.cy, attrs.r).attr(attr).sharp()
},
_getConstantLineLabelsCoords: function(value) {
const center = this.getCenter();
const y = round(center.y - value);
return {
x: center.x,
y: y
}
},
_checkAlignmentConstantLineLabels: _common.noop,
_rotateTick: function(element, coords, isGridLine) {
!isGridLine && element.rotate(coords.angle + 90, coords.x, coords.y)
},
_validateOverlappingMode: circularAxes._validateOverlappingMode,
_validateDisplayMode: circularAxes._validateDisplayMode,
_getStep: function(boxes) {
const quarter = getPolarQuarter(this.getAngles()[0]);
const spacing = this._options.label.minSpacing;
const func = 2 === quarter || 4 === quarter ? function(box) {
return box.width + spacing
} : function(box) {
return box.height
};
const maxLabelLength = boxes.reduce(((prevValue, box) => _max(prevValue, func(box))), 0);
return _axes_constants.default.getTicksCountInRange(this._majorTicks, 2 === quarter || 4 === quarter ? "x" : "y", maxLabelLength)
}
};
const linearSpider = exports.linearSpider = (0, _extend.extend)({}, linear, {
_createPathElement: function(points, attr) {
return this._renderer.path(points, "area").attr(attr).sharp()
},
setSpiderTicks: function(ticks) {
this._spiderTicks = ticks
},
_getGridLineDrawer: function() {
const that = this;
return function(tick, gridStyle) {
return that._createPathElement(that._getGridPoints(tick.coords).points, gridStyle)
}
},
_getGridPoints: function(coords) {
const pos = this.getCenter();
const radius = (0, _utils.getDistance)(pos.x, pos.y, coords.x, coords.y);
return this._getGridPointsByRadius(radius)
},
_getGridPointsByRadius: function(radius) {
const pos = this.getCenter();
if (radius > this.getRadius()) {
return {
points: null
}
}
return {
points: (0, _utils.map)(this._spiderTicks, (function(tick) {
const cosSin = (0, _utils.getCosAndSin)(tick.coords.angle);
return {
x: round(pos.x + radius * cosSin.cos),
y: round(pos.y + radius * cosSin.sin)
}
}))
}
},
_getStripGraphicAttributes: function(fromPoint, toPoint) {
const innerPoints = this._getGridPointsByRadius(toPoint).points;
const outerPoints = this._getGridPointsByRadius(fromPoint).points;
return {
points: [outerPoints, innerPoints.reverse()]
}
},
_createStrip: circularSpider._createStrip,
_getConstantLineGraphicAttributes: function(value) {
return this._getGridPointsByRadius(value)
},
_createConstantLine: function(value, attr) {
return this._createPathElement(this._getConstantLineGraphicAttributes(value).points, attr)
}
});