devextreme
Version:
HTML5 JavaScript Component Suite for Responsive Web Development
463 lines (394 loc) • 17.4 kB
JavaScript
"use strict";
var _format = require("../../core/format"),
vizUtils = require("../../core/utils"),
each = require("../../../core/utils/iterator").each,
extend = require("../../../core/utils/extend").extend,
_degreesToRadians = vizUtils.degreesToRadians,
_patchFontOptions = vizUtils.patchFontOptions,
_math = Math,
_round = _math.round,
_floor = _math.floor,
_abs = _math.abs,
_getCosAndSin = vizUtils.getCosAndSin,
_rotateBBox = vizUtils.rotateBBox,
CONNECTOR_LENGTH = 12,
LABEL_BACKGROUND_PADDING_X = 8,
LABEL_BACKGROUND_PADDING_Y = 4;
function getClosestCoord(point, coords) {
var closestDistance = Infinity,
closestCoord;
each(coords, function (_, coord) {
var x = point[0] - coord[0],
y = point[1] - coord[1],
distance = x * x + y * y;
if (distance < closestDistance) {
closestDistance = distance;
closestCoord = coord;
}
});
return [_floor(closestCoord[0]), _floor(closestCoord[1])];
}
// We could always conside center of label as label point (with appropriate connector path clipping). In that case we do not depend neither on background nor on rotation.
var barPointStrategy = {
isLabelInside: function isLabelInside(labelPoint, figure) {
var xc = labelPoint.x + labelPoint.width / 2,
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 prepareLabelPoints(bBox, rotatedBBox, isHorizontal, angle) {
var x1 = rotatedBBox.x,
xc = x1 + rotatedBBox.width / 2,
x2 = x1 + rotatedBBox.width - 1,
y1 = rotatedBBox.y,
yc = y1 + rotatedBBox.height / 2,
y2 = y1 + rotatedBBox.height - 1;
return _abs(angle) % 90 === 0 ? [[x1, y1], [isHorizontal ? x1 : xc, isHorizontal ? yc : y1], [x2, y1], [x1, y2], [isHorizontal ? x2 : xc, isHorizontal ? yc : y2], [x2, y2]] : [[xc, yc]];
},
isHorizontal: function isHorizontal(bBox, figure) {
return bBox.x > figure.x + figure.width || bBox.x + bBox.width < figure.x;
},
getFigureCenter: function getFigureCenter(figure) {
return [_floor(figure.x + figure.width / 2), _floor(figure.y + figure.height / 2)];
},
findFigurePoint: function findFigurePoint(figure, labelPoint) {
var figureCenter = barPointStrategy.getFigureCenter(figure),
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 adjustPoints(points) {
var lineIsVertical = _abs(points[1] - points[3]) <= 1,
lineIsHorizontal = _abs(points[0] - points[2]) <= 1;
if (lineIsHorizontal) {
points[0] = points[2];
}
if (lineIsVertical) {
points[1] = points[3];
}
return points;
}
};
var symbolPointStrategy = {
isLabelInside: function isLabelInside() {
return false;
},
prepareLabelPoints: barPointStrategy.prepareLabelPoints,
isHorizontal: function isHorizontal(bBox, figure) {
return bBox.x > figure.x + figure.r || bBox.x + bBox.width < figure.x - figure.r;
},
getFigureCenter: function getFigureCenter(figure) {
return [figure.x, figure.y];
},
findFigurePoint: function findFigurePoint(figure, labelPoint) {
var 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
};
var piePointStrategy = {
isLabelInside: function isLabelInside(_0, _1, isOutside) {
return !isOutside;
},
prepareLabelPoints: function prepareLabelPoints(bBox, rotatedBBox, isHorizontal, angle) {
var xl = bBox.x,
xr = xl + bBox.width,
xc = xl + _round(bBox.width / 2),
yt = bBox.y,
yb = yt + bBox.height,
yc = yt + _round(bBox.height / 2),
points = [[[xl, yt], [xr, yt]], [[xr, yt], [xr, yb]], [[xr, yb], [xl, yb]], [[xl, yb], [xl, yt]]],
cosSin = _getCosAndSin(angle);
if (angle === 0) {
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) {
var point1x = pair[0][0],
point1y = pair[0][1],
point2x = pair[1][0],
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 isHorizontal(bBox, figure) {
return bBox.x > figure.x || figure.x > bBox.x + bBox.width;
},
getFigureCenter: symbolPointStrategy.getFigureCenter,
findFigurePoint: function findFigurePoint(figure, labelPoint, isHorizontal) {
if (!isHorizontal) {
return [figure.x, figure.y];
}
var labelX = labelPoint[0],
x = _round(figure.x + (figure.y - labelPoint[1]) / Math.tan(_degreesToRadians(figure.angle))),
points = [figure.x, figure.y, x, labelPoint[1]];
if (!(figure.x <= x && x <= labelX) && !(labelX <= x && x <= figure.x)) {
if (_abs(figure.x - labelX) < CONNECTOR_LENGTH) {
points = [figure.x, figure.y];
} else if (figure.x <= labelX) {
points[2] = figure.x + CONNECTOR_LENGTH;
} else {
points[2] = figure.x - CONNECTOR_LENGTH;
}
}
return points;
},
adjustPoints: function adjustPoints(points) {
return points;
}
};
function selectStrategy(figure) {
return figure.angle !== undefined && piePointStrategy || figure.r !== undefined && symbolPointStrategy || barPointStrategy;
}
function disposeItem(obj, field) {
obj[field] && obj[field].dispose();
obj[field] = null;
}
function checkBackground(background) {
return background && (background.fill && background.fill !== "none" || background["stroke-width"] > 0 && background.stroke && background.stroke !== "none");
}
function checkConnector(connector) {
return connector && connector["stroke-width"] > 0 && connector.stroke && connector.stroke !== "none";
}
function formatText(data, options) {
data.valueText = _format(data.value, options);
data.argumentText = _format(data.argument, { format: options.argumentFormat, precision: options.argumentPrecision /* DEPRECATED_16_1 */ });
if (data.percent !== undefined) {
data.percentText = _format(data.percent, { format: { type: "percent", precision: options.format && options.format.percentPrecision || options.percentPrecision /* DEPRECATED_16_1 */ } });
}
if (data.total !== undefined) {
data.totalText = _format(data.total, options);
}
if (data.openValue !== undefined) {
data.openValueText = _format(data.openValue, options);
}
if (data.closeValue !== undefined) {
data.closeValueText = _format(data.closeValue, options);
}
if (data.lowValue !== undefined) {
data.lowValueText = _format(data.lowValue, options);
}
if (data.highValue !== undefined) {
data.highValueText = _format(data.highValue, options);
}
if (data.reductionValue !== undefined) {
data.reductionValueText = _format(data.reductionValue, options);
}
return options.customizeText ? options.customizeText.call(data, data) : data.valueText;
}
function Label(renderSettings) {
this._renderer = renderSettings.renderer;
this._container = renderSettings.labelsGroup;
this._point = renderSettings.point;
this._strategy = renderSettings.strategy;
}
Label.prototype = {
constructor: Label,
setColor: function setColor(color) {
this._color = color;
},
setOptions: function setOptions(options) {
this._options = options;
},
setData: function setData(data) {
this._data = data;
},
setDataField: function setDataField(fieldName, fieldValue) {
// Is this laziness really required?
this._data = this._data || {};
this._data[fieldName] = fieldValue;
},
getData: function getData() {
return this._data;
},
setFigureToDrawConnector: function setFigureToDrawConnector(figure) {
this._figure = figure;
},
dispose: function dispose() {
var that = this;
disposeItem(that, "_group");
that._data = that._options = that._textContent = that._visible = that._insideGroup = that._text = that._background = that._connector = that._figure = null;
},
// The following method is required because we support partial visibility for labels
// entire labels group can be hidden and any particular label can be visible at the same time
// in order to do that label must have visibility:"visible" attribute
_setVisibility: function _setVisibility(value, state) {
this._group && this._group.attr({ visibility: value });
this._visible = state;
},
isVisible: function isVisible() {
return this._visible;
},
hide: function hide(holdInvisible) {
this._holdVisibility = !!holdInvisible;
this._hide();
},
_hide: function _hide() {
this._setVisibility("hidden", false);
},
show: function show(holdVisible) {
var correctPosition = !this._drawn;
if (this._point.hasValue()) {
this._holdVisibility = !!holdVisible;
this._show();
correctPosition && this._point.correctLabelPosition(this);
}
},
_show: function _show() {
var that = this,
renderer = that._renderer,
container = that._container,
options = that._options || {},
text = that._textContent = formatText(that._data, that._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 ? _patchFontOptions(options.attributes.font) : {});
if (checkBackground(options.background)) {
that._background = that._background || renderer.rect().append(that._insideGroup).toBackground();
that._background.attr(options.background);
// The following is because "this._options" is shared between all labels and so cannot be modified
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);
// The following is because "this._options" is shared between all labels and so cannot be modified
that._color && that._connector.attr({ stroke: that._color });
} else {
disposeItem(that, "_connector");
}
that._text.attr({ text: text, align: options.textAlignment });
that._updateBackground(that._text.getBBox());
that._setVisibility("visible", true);
that._drawn = true;
} else {
that._hide();
}
},
_getLabelVisibility: function _getLabelVisibility(isVisible) {
return this._holdVisibility ? this.isVisible() : isVisible;
},
draw: function draw(isVisible) {
if (this._getLabelVisibility(isVisible)) {
this._show();
this._point && this._point.correctLabelPosition(this);
} else {
this._drawn = false;
this._hide();
}
return this;
},
_updateBackground: function _updateBackground(bBox) {
var that = this;
if (that._background) {
bBox.x -= LABEL_BACKGROUND_PADDING_X;
bBox.y -= LABEL_BACKGROUND_PADDING_Y;
bBox.width += 2 * LABEL_BACKGROUND_PADDING_X;
bBox.height += 2 * LABEL_BACKGROUND_PADDING_Y;
that._background.attr(bBox);
}
that._bBoxWithoutRotation = extend({}, bBox);
if (that._options.rotationAngle) {
that._insideGroup.rotate(that._options.rotationAngle, bBox.x + bBox.width / 2, bBox.y + bBox.height / 2);
// Angle is transformed from svg to right-handed cartesian space
bBox = _rotateBBox(bBox, [bBox.x + bBox.width / 2, bBox.y + bBox.height / 2], -that._options.rotationAngle);
}
that._bBox = bBox;
},
_getConnectorPoints: function _getConnectorPoints() {
var that = this,
figure = that._figure,
options = that._options,
strategy = that._strategy || selectStrategy(figure),
bBox = that._shiftBBox(that._bBoxWithoutRotation),
rotatedBBox = that.getBoundingRect(),
labelPoint,
points = [],
isHorizontal;
if (!strategy.isLabelInside(bBox, figure, options.position !== "inside")) {
isHorizontal = strategy.isHorizontal(bBox, figure);
points = strategy.prepareLabelPoints(bBox, rotatedBBox, isHorizontal, -options.rotationAngle || 0);
labelPoint = getClosestCoord(strategy.getFigureCenter(figure), points);
points = strategy.findFigurePoint(figure, labelPoint, isHorizontal);
points = points.concat(labelPoint);
}
return strategy.adjustPoints(points);
},
// TODO: Should not be called when not invisible (check for "_textContent" is to be removed)
fit: function fit(maxWidth) {
var padding = this._background ? 2 * LABEL_BACKGROUND_PADDING_X : 0;
this._text && this._text.applyEllipsis(maxWidth - padding);
this._updateBackground(this._text.getBBox());
},
resetEllipsis: function resetEllipsis() {
this._text && this._text.restoreText();
this._updateBackground(this._text.getBBox());
},
setTrackerData: function setTrackerData(point) {
this._text.data({ "chart-data-point": point });
this._background && this._background.data({ "chart-data-point": point });
},
hideInsideLabel: function hideInsideLabel(coords) {
return this._point.hideInsideLabel(this, coords);
},
// TODO: Should not be called when not invisible (check for "_textContent" is to be removed)
shift: function shift(x, y) {
var 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;
},
// TODO: Should not be called when not invisible (check for "_textContent" is to be removed)
getBoundingRect: function getBoundingRect() {
return this._shiftBBox(this._bBox);
},
_shiftBBox: function _shiftBBox(bBox) {
return this._textContent ? {
x: bBox.x + this._x,
y: bBox.y + this._y,
width: bBox.width,
height: bBox.height
} : {};
},
getLayoutOptions: function getLayoutOptions() {
var 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) ? CONNECTOR_LENGTH : 0) + (checkBackground(options.background) ? LABEL_BACKGROUND_PADDING_X : 0)
};
}
};
exports.Label = Label;
///#DEBUG
Label._DEBUG_formatText = formatText;
///#ENDDEBUG