devextreme
Version:
HTML5 JavaScript Component Suite for Responsive Web Development
507 lines (497 loc) • 19.1 kB
JavaScript
/**
* DevExtreme (cjs/viz/series/points/label.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.Label = Label;
var _format_helper = _interopRequireDefault(require("../../../format_helper"));
var _utils = require("../../core/utils");
var _iterator = require("../../../core/utils/iterator");
var _extend = require("../../../core/utils/extend");
var _display_format_parser = require("../helpers/display_format_parser");
function _interopRequireDefault(e) {
return e && e.__esModule ? e : {
default: e
}
}
const _format = _format_helper.default.format;
const _math = Math;
const _round = _math.round;
const _floor = _math.floor;
const _abs = _math.abs;
const CONNECTOR_LENGTH = 12;
const LABEL_BACKGROUND_PADDING_X = 8;
const LABEL_BACKGROUND_PADDING_Y = 4;
function getClosestCoord(point, coords) {
let closestDistance = 1 / 0;
let closestCoord;
(0, _iterator.each)(coords, (function(_, coord) {
const x = point[0] - coord[0];
const y = point[1] - coord[1];
const distance = x * x + y * y;
if (distance < closestDistance) {
closestDistance = distance;
closestCoord = coord
}
}));
return [_floor(closestCoord[0]), _floor(closestCoord[1])]
}
function getCrossCoord(rect, coord, indexOffset) {
return (coord - rect[0 + indexOffset]) / (rect[2 + indexOffset] - rect[0 + indexOffset]) * (rect[3 - indexOffset] - rect[1 - indexOffset]) + rect[1 - indexOffset]
}
const barPointStrategy = {
isLabelInside: function(labelPoint, figure) {
const xc = labelPoint.x + labelPoint.width / 2;
const yc = labelPoint.y + labelPoint.height / 2;
return figure.x <= xc && xc <= figure.x + figure.width && figure.y <= yc && yc <= figure.y + figure.height
},
prepareLabelPoints: function(bBox, rotatedBBox, isHorizontal, angle, figureCenter) {
const x1 = rotatedBBox.x;
const xc = x1 + rotatedBBox.width / 2;
const x2 = x1 + rotatedBBox.width - 1;
const y1 = rotatedBBox.y;
const yc = y1 + rotatedBBox.height / 2;
const y2 = y1 + rotatedBBox.height - 1;
let labelPoints;
const isRectangular = _abs(angle) % 90 === 0;
if (figureCenter[0] > x1 && figureCenter[0] < x2) {
if (isRectangular) {
labelPoints = [
[figureCenter[0], _abs(figureCenter[1] - y1) < _abs(figureCenter[1] - y2) ? y1 : y2]
]
} else {
labelPoints = [
[figureCenter[0], getCrossCoord([x1, y1, x2, y2], figureCenter[0], 0)]
]
}
} else if (figureCenter[1] > y1 && figureCenter[1] < y2) {
if (isRectangular) {
labelPoints = [
[_abs(figureCenter[0] - x1) < _abs(figureCenter[0] - x2) ? x1 : x2, figureCenter[1]]
]
} else {
labelPoints = [
[getCrossCoord([x1, y1, x2, y2], figureCenter[1], 1), figureCenter[1]]
]
}
} else if (isRectangular) {
labelPoints = [
[x1, y1],
[isHorizontal ? x1 : xc, isHorizontal ? yc : y1],
[x2, y1],
[x1, y2],
[isHorizontal ? x2 : xc, isHorizontal ? yc : y2],
[x2, y2]
]
} else {
labelPoints = [
[xc, yc]
]
}
return labelPoints
},
isHorizontal: function(bBox, figure) {
return bBox.x > figure.x + figure.width || bBox.x + bBox.width < figure.x
},
getFigureCenter: function(figure) {
return [_floor(figure.x + figure.width / 2), _floor(figure.y + figure.height / 2)]
},
findFigurePoint: function(figure, labelPoint) {
const figureCenter = barPointStrategy.getFigureCenter(figure);
const point = getClosestCoord(labelPoint, [
[figure.x, figureCenter[1]],
[figureCenter[0], figure.y + figure.height],
[figure.x + figure.width, figureCenter[1]],
[figureCenter[0], figure.y]
]);
return point
},
adjustPoints: function(points) {
const lineIsVertical = _abs(points[1] - points[3]) <= 1;
const lineIsHorizontal = _abs(points[0] - points[2]) <= 1;
if (lineIsHorizontal) {
points[0] = points[2]
}
if (lineIsVertical) {
points[1] = points[3]
}
return points
}
};
const symbolPointStrategy = {
isLabelInside: function() {
return false
},
prepareLabelPoints: barPointStrategy.prepareLabelPoints,
isHorizontal: function(bBox, figure) {
return bBox.x > figure.x + figure.r || bBox.x + bBox.width < figure.x - figure.r
},
getFigureCenter: function(figure) {
return [figure.x, figure.y]
},
findFigurePoint: function(figure, labelPoint) {
const angle = Math.atan2(figure.y - labelPoint[1], labelPoint[0] - figure.x);
return [_round(figure.x + figure.r * Math.cos(angle)), _round(figure.y - figure.r * Math.sin(angle))]
},
adjustPoints: barPointStrategy.adjustPoints
};
const piePointStrategy = {
isLabelInside: function(_0, _1, isOutside) {
return !isOutside
},
prepareLabelPoints: function(bBox, rotatedBBox, isHorizontal, angle) {
const xl = bBox.x;
const xr = xl + bBox.width;
const xc = xl + _round(bBox.width / 2);
const yt = bBox.y;
const yb = yt + bBox.height;
const yc = yt + _round(bBox.height / 2);
let points = [
[
[xl, yt],
[xr, yt]
],
[
[xr, yt],
[xr, yb]
],
[
[xr, yb],
[xl, yb]
],
[
[xl, yb],
[xl, yt]
]
];
const cosSin = (0, _utils.getCosAndSin)(angle);
if (0 === angle) {
points = isHorizontal ? [
[xl, yc],
[xr, yc]
] : [
[xc, yt],
[xc, yb]
]
} else {
points = points.map((function(pair) {
return pair.map((function(point) {
return [_round((point[0] - xc) * cosSin.cos + (point[1] - yc) * cosSin.sin + xc), _round(-(point[0] - xc) * cosSin.sin + (point[1] - yc) * cosSin.cos + yc)]
}))
})).reduce((function(r, pair) {
const point1x = pair[0][0];
const point1y = pair[0][1];
const point2x = pair[1][0];
const point2y = pair[1][1];
if (isHorizontal) {
if (point1y >= yc && yc >= point2y || point1y <= yc && yc <= point2y) {
r.push([(yc - point1y) * (point2x - point1x) / (point2y - point1y) + point1x, yc])
}
} else if (point1x >= xc && xc >= point2x || point1x <= xc && xc <= point2x) {
r.push([xc, (xc - point1x) * (point2y - point1y) / (point2x - point1x) + point1y])
}
return r
}), [])
}
return points
},
isHorizontal: function(bBox, figure) {
return bBox.x > figure.x || figure.x > bBox.x + bBox.width
},
getFigureCenter: symbolPointStrategy.getFigureCenter,
findFigurePoint: function(figure, labelPoint, isHorizontal) {
if (!isHorizontal) {
return [figure.x, figure.y]
}
const labelX = labelPoint[0];
const x = _round(figure.x + (figure.y - labelPoint[1]) / Math.tan((0, _utils.degreesToRadians)(figure.angle)));
let points = [figure.x, figure.y, x, labelPoint[1]];
if (!(figure.x <= x && x <= labelX) && !(labelX <= x && x <= figure.x)) {
if (_abs(figure.x - labelX) < 12) {
points = [figure.x, figure.y]
} else if (figure.x <= labelX) {
points[2] = figure.x + 12
} else {
points[2] = figure.x - 12
}
}
return points
},
adjustPoints: function(points) {
return points
}
};
function selectStrategy(figure) {
return void 0 !== figure.angle && piePointStrategy || void 0 !== figure.r && symbolPointStrategy || barPointStrategy
}
function disposeItem(obj, field) {
obj[field] && obj[field].dispose();
obj[field] = null
}
function checkBackground(background) {
return background && (background.fill && "none" !== background.fill || background["stroke-width"] > 0 && background.stroke && "none" !== background.stroke)
}
function checkConnector(connector) {
return connector && connector["stroke-width"] > 0 && connector.stroke && "none" !== connector.stroke
}
function formatText(data, options) {
const format = options.format;
data.valueText = _format(data.value, format);
data.argumentText = _format(data.argument, options.argumentFormat);
if (void 0 !== data.percent) {
data.percentText = _format(data.percent, {
type: "percent",
precision: format && format.percentPrecision
})
}
if (void 0 !== data.total) {
data.totalText = _format(data.total, format)
}
if (void 0 !== data.openValue) {
data.openValueText = _format(data.openValue, format)
}
if (void 0 !== data.closeValue) {
data.closeValueText = _format(data.closeValue, format)
}
if (void 0 !== data.lowValue) {
data.lowValueText = _format(data.lowValue, format)
}
if (void 0 !== data.highValue) {
data.highValueText = _format(data.highValue, format)
}
if (void 0 !== data.reductionValue) {
data.reductionValueText = _format(data.reductionValue, format)
}
return options.customizeText ? options.customizeText.call(data, data) : options.displayFormat ? (0, _display_format_parser.processDisplayFormat)(options.displayFormat, data) : data.valueText
}
function Label(renderSettings) {
this._renderer = renderSettings.renderer;
this._container = renderSettings.labelsGroup;
this._point = renderSettings.point;
this._strategy = renderSettings.strategy;
this._rowCount = 1
}
Label.prototype = {
constructor: Label,
setColor: function(color) {
this._color = color
},
setOptions: function(options) {
this._options = options
},
setData: function(data) {
this._data = data
},
setDataField: function(fieldName, fieldValue) {
this._data = this._data || {};
this._data[fieldName] = fieldValue
},
getData: function() {
return this._data
},
setFigureToDrawConnector: function(figure) {
this._figure = figure
},
dispose: function() {
disposeItem(this, "_group");
this._data = this._options = this._textContent = this._visible = this._insideGroup = this._text = this._background = this._connector = this._figure = null
},
_setVisibility: function(value, state) {
this._group && this._group.attr({
visibility: value
});
this._visible = state
},
isVisible: function() {
return this._visible
},
hide: function(holdInvisible) {
this._holdVisibility = !!holdInvisible;
this._hide()
},
_hide: function() {
this._setVisibility("hidden", false)
},
show: function(holdVisible) {
const correctPosition = !this._drawn;
if (this._point.hasValue()) {
this._holdVisibility = !!holdVisible;
this._show();
correctPosition && this._point.correctLabelPosition(this)
}
},
_show: function() {
const that = this;
const renderer = that._renderer;
const container = that._container;
const options = that._options || {};
const text = that._textContent = formatText(that._data, options) || null;
if (text) {
if (!that._group) {
that._group = renderer.g().append(container);
that._insideGroup = renderer.g().append(that._group);
that._text = renderer.text("", 0, 0).append(that._insideGroup)
}
that._text.css(options.attributes ? (0, _utils.patchFontOptions)(options.attributes.font) : {});
if (checkBackground(options.background)) {
that._background = that._background || renderer.rect().append(that._insideGroup).toBackground();
that._background.attr(options.background);
that._color && that._background.attr({
fill: that._color
})
} else {
disposeItem(that, "_background")
}
if (checkConnector(options.connector)) {
that._connector = that._connector || renderer.path([], "line").sharp().append(that._group).toBackground();
that._connector.attr(options.connector);
that._color && that._connector.attr({
stroke: that._color
})
} else {
disposeItem(that, "_connector")
}
that._text.attr({
text: text,
align: options.textAlignment,
class: options.cssClass
});
that._updateBackground(that._text.getBBox());
that._setVisibility("visible", true);
that._drawn = true
} else {
that._hide()
}
},
_getLabelVisibility: function(isVisible) {
return this._holdVisibility ? this.isVisible() : isVisible
},
draw: function(isVisible) {
if (this._getLabelVisibility(isVisible)) {
this._show();
this._point && this._point.correctLabelPosition(this)
} else {
this._drawn = false;
this._hide()
}
return this
},
_updateBackground: function(bBox) {
const that = this;
if (that._background) {
bBox.x -= 8;
bBox.y -= 4;
bBox.width += 16;
bBox.height += 8;
that._background.attr(bBox)
}
that._bBoxWithoutRotation = (0, _extend.extend)({}, bBox);
const rotationAngle = that._options.rotationAngle || 0;
that._insideGroup.rotate(rotationAngle, bBox.x + bBox.width / 2, bBox.y + bBox.height / 2);
bBox = (0, _utils.rotateBBox)(bBox, [bBox.x + bBox.width / 2, bBox.y + bBox.height / 2], -rotationAngle);
that._bBox = bBox
},
getFigureCenter() {
const figure = this._figure;
const strategy = this._strategy || selectStrategy(figure);
return strategy.getFigureCenter(figure)
},
_getConnectorPoints: function() {
const that = this;
const figure = that._figure;
const options = that._options;
const strategy = that._strategy || selectStrategy(figure);
const bBox = that._shiftBBox(that._bBoxWithoutRotation);
const rotatedBBox = that.getBoundingRect();
let labelPoint;
let points = [];
let isHorizontal;
if (!strategy.isLabelInside(bBox, figure, "inside" !== options.position)) {
isHorizontal = strategy.isHorizontal(bBox, figure);
const figureCenter = that.getFigureCenter();
points = strategy.prepareLabelPoints(bBox, rotatedBBox, isHorizontal, -options.rotationAngle || 0, figureCenter);
labelPoint = getClosestCoord(figureCenter, points);
points = strategy.findFigurePoint(figure, labelPoint, isHorizontal);
points = points.concat(labelPoint)
}
return strategy.adjustPoints(points)
},
fit: function(maxWidth) {
const padding = this._background ? 16 : 0;
let rowCountChanged = false;
if (this._text) {
const result = this._text.setMaxSize(maxWidth - padding, void 0, this._options);
let rowCount = result.rowCount;
if (0 === rowCount) {
rowCount = 1
}
if (rowCount !== this._rowCount) {
rowCountChanged = true;
this._rowCount = rowCount
}
result.textIsEmpty && disposeItem(this, "_background")
}
this._updateBackground(this._text.getBBox());
return rowCountChanged
},
resetEllipsis: function() {
this._text && this._text.restoreText();
this._updateBackground(this._text.getBBox())
},
setTrackerData: function(point) {
this._text.data({
"chart-data-point": point
});
this._background && this._background.data({
"chart-data-point": point
})
},
hideInsideLabel: function(coords) {
return this._point.hideInsideLabel(this, coords)
},
getPoint() {
return this._point
},
shift: function(x, y) {
const that = this;
if (that._textContent) {
that._insideGroup.attr({
translateX: that._x = _round(x - that._bBox.x),
translateY: that._y = _round(y - that._bBox.y)
});
if (that._connector) {
that._connector.attr({
points: that._getConnectorPoints()
})
}
}
return that
},
getBoundingRect: function() {
return this._shiftBBox(this._bBox)
},
_shiftBBox: function(bBox) {
return this._textContent ? {
x: bBox.x + this._x,
y: bBox.y + this._y,
width: bBox.width,
height: bBox.height
} : {}
},
getLayoutOptions: function() {
const options = this._options;
return {
alignment: options.alignment,
background: checkBackground(options.background),
horizontalOffset: options.horizontalOffset,
verticalOffset: options.verticalOffset,
radialOffset: options.radialOffset,
position: options.position,
connectorOffset: (checkConnector(options.connector) ? 12 : 0) + (checkBackground(options.background) ? 8 : 0)
}
}
};