highcharts
Version:
JavaScript charting framework
611 lines (610 loc) • 26.6 kB
JavaScript
/* *
*
* (c) 2010-2025 Torstein Honsi
*
* License: www.highcharts.com/license
*
* !!!!!!! SOURCE GETS TRANSPILED BY TYPESCRIPT. EDIT TS FILE ONLY. !!!!!!!
*
* */
;
import A from '../Animation/AnimationUtilities.js';
const { getDeferredAnimation } = A;
import F from '../Templating.js';
const { format } = F;
import U from '../Utilities.js';
const { defined, extend, fireEvent, getAlignFactor, isArray, isString, merge, objectEach, pick, pInt, splat } = U;
/* *
*
* Composition
*
* */
var DataLabel;
(function (DataLabel) {
/* *
*
* Declarations
*
* */
/* *
*
* Functions
*
* */
/**
* Check if this series has data labels, either a series-level setting, or
* individual. In case of individual point labels, this method is overridden
* to always return true.
* @private
*/
function hasDataLabels() {
return mergedDataLabelOptions(this)
.some((o) => o?.enabled);
}
/**
* Align each individual data label.
* @private
*/
function alignDataLabel(point, dataLabel, options, alignTo, isNew) {
const series = this, { chart, enabledDataSorting } = this, inverted = this.isCartesian && chart.inverted, plotX = point.plotX, plotY = point.plotY, rotation = options.rotation || 0, isInsidePlot = defined(plotX) &&
defined(plotY) &&
chart.isInsidePlot(plotX, Math.round(plotY), {
inverted,
paneCoordinates: true,
series
}), setStartPos = (alignOptions) => {
if (enabledDataSorting && series.xAxis && !justify) {
series.setDataLabelStartPos(point, dataLabel, isNew, isInsidePlot, alignOptions);
}
}, justify = rotation === 0 ? pick(options.overflow, (enabledDataSorting ? 'none' : 'justify')) === 'justify' : false;
// Math.round for rounding errors (#2683), alignTo to allow column
// labels (#2700)
let visible = this.visible &&
point.visible !== false &&
defined(plotX) &&
(point.series.forceDL ||
(enabledDataSorting && !justify) ||
isInsidePlot ||
(
// If the data label is inside the align box, it is enough
// that parts of the align box is inside the plot area
// (#12370). When stacking, it is always inside regardless
// of the option (#15148).
pick(options.inside, !!this.options.stacking) &&
alignTo &&
chart.isInsidePlot(plotX, inverted ?
alignTo.x + 1 :
alignTo.y + alignTo.height - 1, {
inverted,
paneCoordinates: true,
series
})));
const pos = point.pos();
if (visible && pos) {
const bBox = dataLabel.getBBox(), unrotatedbBox = dataLabel.getBBox(void 0, 0);
// The alignment box is a singular point
alignTo = extend({
x: pos[0],
y: Math.round(pos[1]),
width: 0,
height: 0
}, alignTo || {});
// Align to plot edges
if (options.alignTo === 'plotEdges' && series.isCartesian) {
alignTo[inverted ? 'x' : 'y'] = 0;
alignTo[inverted ? 'width' : 'height'] = this.yAxis?.len || 0;
}
// Add the text size for alignment calculation
extend(options, {
width: bBox.width,
height: bBox.height
});
setStartPos(alignTo); // Data sorting
// Align the label to the adjusted box with for unrotated bBox due
// to rotationOrigin, which is based on unrotated label
dataLabel.align(merge(options, {
width: unrotatedbBox.width,
height: unrotatedbBox.height
}), false, alignTo, false);
dataLabel.alignAttr.x += getAlignFactor(options.align) *
(unrotatedbBox.width - bBox.width);
dataLabel.alignAttr.y += getAlignFactor(options.verticalAlign) *
(unrotatedbBox.height - bBox.height);
dataLabel[dataLabel.placed ? 'animate' : 'attr']({
'text-align': dataLabel.alignAttr['text-align'] || 'center',
x: dataLabel.alignAttr.x +
(bBox.width - unrotatedbBox.width) / 2,
y: dataLabel.alignAttr.y +
(bBox.height - unrotatedbBox.height) / 2,
rotationOriginX: (dataLabel.width || 0) / 2,
rotationOriginY: (dataLabel.height || 0) / 2
});
// Uncomment this block to visualize the bounding boxes used for
// determining visibility
// chart.renderer.rect(
// (dataLabel.alignAttr.x || 0) + chart.plotLeft,
// (dataLabel.alignAttr.y || 0) + chart.plotTop,
// bBox.width,
// bBox.height
// ).attr({
// stroke: 'rgba(0, 0, 0, 0.3)',
// 'stroke-width': 1,
// zIndex: 20
// }).add();
// chart.renderer.circle(
// chart.plotLeft + pick(dataLabel.alignAttr.x, 0),
// chart.plotTop + pick(dataLabel.alignAttr.y, 0),
// 2
// ).attr({
// fill: 'red',
// zIndex: 20
// }).add();
if (justify && alignTo.height >= 0) { // #8830
this.justifyDataLabel(dataLabel, options, dataLabel.alignAttr, bBox, alignTo, isNew);
}
else if (pick(options.crop, true)) {
const { x, y } = dataLabel.alignAttr, correction = 1;
// Check if the dataLabel should be visible.
visible =
chart.isInsidePlot(x, y, {
paneCoordinates: true,
series
}) &&
chart.isInsidePlot(x + bBox.width - correction, y + bBox.height - correction, {
paneCoordinates: true,
series
});
}
// When we're using a shape, make it possible with a connector or an
// arrow pointing to this point
if (options.shape && !rotation) {
dataLabel[isNew ? 'attr' : 'animate']({
anchorX: pos[0],
anchorY: pos[1]
});
}
}
// To use alignAttr property in hideOverlappingLabels
if (isNew && enabledDataSorting) {
dataLabel.placed = false;
}
// Show or hide based on the final aligned position
if (!visible && (!enabledDataSorting || justify)) {
dataLabel.hide();
dataLabel.placed = false; // Don't animate back in
}
else {
dataLabel.show();
dataLabel.placed = true; // Flag for overlapping logic
}
}
/**
* Handle the dataLabels.filter option.
* @private
*/
function applyFilter(point, options) {
const filter = options.filter;
if (filter) {
const op = filter.operator, prop = point[filter.property], val = filter.value;
if ((op === '>' && prop > val) ||
(op === '<' && prop < val) ||
(op === '>=' && prop >= val) ||
(op === '<=' && prop <= val) ||
(op === '==' && prop == val) || // eslint-disable-line eqeqeq
(op === '===' && prop === val) ||
(op === '!=' && prop != val) || // eslint-disable-line eqeqeq
(op === '!==' && prop !== val)) {
return true;
}
return false;
}
return true;
}
/**
* @private
*/
function compose(SeriesClass) {
const seriesProto = SeriesClass.prototype;
if (!seriesProto.initDataLabels) {
seriesProto.initDataLabels = initDataLabels;
seriesProto.initDataLabelsGroup = initDataLabelsGroup;
seriesProto.alignDataLabel = alignDataLabel;
seriesProto.drawDataLabels = drawDataLabels;
seriesProto.justifyDataLabel = justifyDataLabel;
seriesProto.mergeArrays = mergeArrays;
seriesProto.setDataLabelStartPos = setDataLabelStartPos;
seriesProto.hasDataLabels = hasDataLabels;
}
}
DataLabel.compose = compose;
/**
* Create the SVGElement group for dataLabels
* @private
*/
function initDataLabelsGroup() {
return this.plotGroup('dataLabelsGroup', 'data-labels', this.hasRendered ? 'inherit' : 'hidden', // #5133, #10220
this.options.dataLabels.zIndex || 6);
}
/**
* Init the data labels with the correct animation
* @private
*/
function initDataLabels(animationConfig) {
const series = this, hasRendered = series.hasRendered || 0;
// Create a separate group for the data labels to avoid rotation
const dataLabelsGroup = this.initDataLabelsGroup()
.attr({ opacity: +hasRendered }); // #3300
if (!hasRendered && dataLabelsGroup) {
if (series.visible) { // #2597, #3023, #3024
dataLabelsGroup.show();
}
if (series.options.animation) {
dataLabelsGroup.animate({ opacity: 1 }, animationConfig);
}
else {
dataLabelsGroup.attr({ opacity: 1 });
}
}
return dataLabelsGroup;
}
/**
* Draw the data labels
* @private
*/
function drawDataLabels(points) {
points = points || this.points;
const series = this, chart = series.chart, seriesOptions = series.options, renderer = chart.renderer, { backgroundColor, plotBackgroundColor } = chart.options.chart, contrastColor = renderer.getContrast((isString(plotBackgroundColor) && plotBackgroundColor) ||
(isString(backgroundColor) && backgroundColor) ||
"#000000" /* Palette.neutralColor100 */), seriesDlOptions = mergedDataLabelOptions(series);
let pointOptions, dataLabelsGroup;
// Resolve the animation
const { animation, defer } = seriesDlOptions[0], animationConfig = defer ?
getDeferredAnimation(chart, animation, series) :
{ defer: 0, duration: 0 };
fireEvent(this, 'drawDataLabels');
if (series.hasDataLabels?.()) {
dataLabelsGroup = this.initDataLabels(animationConfig);
// Make the labels for each point
points.forEach((point) => {
const dataLabels = point.dataLabels || [], pointColor = point.color || series.color;
// Merge in series options for the point.
// @note dataLabelAttribs (like pointAttribs) would eradicate
// the need for dlOptions, and simplify the section below.
pointOptions = splat(mergeArrays(seriesDlOptions,
// The dlOptions prop is used in treemaps
point.dlOptions || point.options?.dataLabels));
// Handle each individual data label for this point
pointOptions.forEach((labelOptions, i) => {
// Options for one datalabel
const labelEnabled = (labelOptions.enabled &&
(point.visible || point.dataLabelOnHidden) &&
// #2282, #4641, #7112, #10049
(!point.isNull || point.dataLabelOnNull) &&
applyFilter(point, labelOptions)), { backgroundColor, borderColor, distance, style = {} } = labelOptions;
let formatString, labelText, rotation, attr = {}, dataLabel = dataLabels[i], isNew = !dataLabel, labelBgColor;
if (labelEnabled) {
// Create individual options structure that can be
// extended without affecting others
formatString = pick(labelOptions[point.formatPrefix + 'Format'], labelOptions.format);
labelText = defined(formatString) ?
format(formatString, point, chart) :
(labelOptions[point.formatPrefix + 'Formatter'] ||
labelOptions.formatter).call(point, labelOptions);
rotation = labelOptions.rotation;
if (!chart.styledMode) {
// Determine the color
style.color = pick(labelOptions.color, style.color, isString(series.color) ? series.color : void 0, "#000000" /* Palette.neutralColor100 */);
// Get automated contrast color
if (style.color === 'contrast') {
if (backgroundColor !== 'none') {
labelBgColor = backgroundColor;
}
point.contrastColor = renderer.getContrast((labelBgColor !== 'auto' &&
isString(labelBgColor) &&
labelBgColor) ||
(isString(pointColor) ? pointColor : ''));
style.color = (labelBgColor || // #20007
(!defined(distance) &&
labelOptions.inside) ||
pInt(distance || 0) < 0 ||
seriesOptions.stacking) ?
point.contrastColor :
contrastColor;
}
else {
delete point.contrastColor;
}
if (seriesOptions.cursor) {
style.cursor = seriesOptions.cursor;
}
}
attr = {
r: labelOptions.borderRadius || 0,
rotation,
padding: labelOptions.padding,
zIndex: 1
};
if (!chart.styledMode) {
attr.fill = backgroundColor === 'auto' ?
point.color :
backgroundColor;
attr.stroke = borderColor === 'auto' ?
point.color :
borderColor;
attr['stroke-width'] = labelOptions.borderWidth;
}
// Remove unused attributes (#947)
objectEach(attr, (val, name) => {
if (typeof val === 'undefined') {
delete attr[name];
}
});
}
// If the point is outside the plot area, or the label
// changes properties that we cannot change, destroy it and
// build a new one below. #678, #820.
if (dataLabel && (!labelEnabled ||
!defined(labelText) ||
// Changed useHTML value
!!(dataLabel.div ||
dataLabel.text?.foreignObject) !== !!labelOptions.useHTML ||
(
// Change from no rotation to rotation and
// vice versa. Don't use defined() because
// rotation = 0 means also rotation = undefined
(!dataLabel.rotation ||
!labelOptions.rotation) &&
dataLabel.rotation !== labelOptions.rotation))) {
dataLabel = void 0;
isNew = true;
}
// Individual labels are disabled if the are explicitly
// disabled in the point options, or if they fall outside
// the plot area.
if (labelEnabled && defined(labelText)) {
if (!dataLabel) {
// Create new label element
dataLabel = renderer.label(labelText, 0, 0, labelOptions.shape, void 0, void 0, labelOptions.useHTML, void 0, 'data-label');
dataLabel.addClass(' highcharts-data-label-color-' +
point.colorIndex +
' ' + (labelOptions.className || '') +
( // #3398
labelOptions.useHTML ?
' highcharts-tracker' :
''));
}
else {
// Use old element and just update text
attr.text = labelText;
}
// Store data label options for later access
if (dataLabel) {
dataLabel.options = labelOptions;
dataLabel.attr(attr);
if (!chart.styledMode) {
// Styles must be applied before add in order to
// read text bounding box
dataLabel.css(style).shadow(labelOptions.shadow);
}
else if (style.width) {
// In styled mode with a width property set,
// the width should be applied to the
// dataLabel. (#20499). These properties affect
// layout and must be applied also in styled
// mode.
dataLabel.css({
width: style.width,
textOverflow: style.textOverflow,
whiteSpace: style.whiteSpace
});
}
fireEvent(dataLabel, 'beforeAddingDataLabel', { labelOptions, point });
if (!dataLabel.added) {
dataLabel.add(dataLabelsGroup);
}
// Now the data label is created and placed at 0,0,
// so we need to align it
series.alignDataLabel(point, dataLabel, labelOptions, void 0, isNew);
dataLabel.isActive = true;
if (dataLabels[i] && dataLabels[i] !== dataLabel) {
dataLabels[i].destroy();
}
dataLabels[i] = dataLabel;
}
}
});
// Destroy and remove the inactive ones
let j = dataLabels.length;
while (j--) {
// The item can be undefined if a disabled data label is
// succeeded by an enabled one (#19457)
if (!dataLabels[j]?.isActive) {
dataLabels[j]?.destroy();
dataLabels.splice(j, 1);
}
else {
dataLabels[j].isActive = false;
}
}
// Write back
point.dataLabel = dataLabels[0];
point.dataLabels = dataLabels;
});
}
fireEvent(this, 'afterDrawDataLabels');
}
/**
* If data labels fall partly outside the plot area, align them back in, in
* a way that doesn't hide the point.
* @private
*/
function justifyDataLabel(dataLabel, options, alignAttr, bBox, alignTo, isNew) {
const chart = this.chart, align = options.align, verticalAlign = options.verticalAlign, padding = dataLabel.box ? 0 : (dataLabel.padding || 0), horizontalAxis = chart.inverted ? this.yAxis : this.xAxis, horizontalAxisShift = horizontalAxis ?
horizontalAxis.left - chart.plotLeft : 0, verticalAxis = chart.inverted ? this.xAxis : this.yAxis, verticalAxisShift = verticalAxis ?
verticalAxis.top - chart.plotTop : 0;
let { x = 0, y = 0 } = options, off, justified;
// Off left
off = (alignAttr.x || 0) + padding + horizontalAxisShift;
if (off < 0) {
if (align === 'right' && x >= 0) {
options.align = 'left';
options.inside = true;
}
else {
x -= off;
}
justified = true;
}
// Off right
off = (alignAttr.x || 0) + bBox.width - padding + horizontalAxisShift;
if (off > chart.plotWidth) {
if (align === 'left' && x <= 0) {
options.align = 'right';
options.inside = true;
}
else {
x += chart.plotWidth - off;
}
justified = true;
}
// Off top
off = alignAttr.y + padding + verticalAxisShift;
if (off < 0) {
if (verticalAlign === 'bottom' && y >= 0) {
options.verticalAlign = 'top';
options.inside = true;
}
else {
y -= off;
}
justified = true;
}
// Off bottom
off = (alignAttr.y || 0) + bBox.height - padding + verticalAxisShift;
if (off > chart.plotHeight) {
if (verticalAlign === 'top' && y <= 0) {
options.verticalAlign = 'bottom';
options.inside = true;
}
else {
y += chart.plotHeight - off;
}
justified = true;
}
if (justified) {
options.x = x;
options.y = y;
dataLabel.placed = !isNew;
dataLabel.align(options, void 0, alignTo);
}
return justified;
}
/**
* Merge two objects that can be arrays. If one of them is an array, the
* other is merged into each element. If both are arrays, each element is
* merged by index. If neither are arrays, we use normal merge.
* @private
*/
function mergeArrays(one, two) {
let res = [], i;
if (isArray(one) && !isArray(two)) {
res = one.map(function (el) {
return merge(el, two);
});
}
else if (isArray(two) && !isArray(one)) {
res = two.map(function (el) {
return merge(one, el);
});
}
else if (!isArray(one) && !isArray(two)) {
res = merge(one, two);
}
else if (isArray(one) && isArray(two)) {
i = Math.max(one.length, two.length);
while (i--) {
res[i] = merge(one[i], two[i]);
}
}
return res;
}
/**
* Merge plotOptions and series options for dataLabels.
* @private
*/
function mergedDataLabelOptions(series) {
const plotOptions = series.chart.options.plotOptions;
return splat(mergeArrays(mergeArrays(plotOptions?.series?.dataLabels, plotOptions?.[series.type]?.dataLabels), series.options.dataLabels));
}
/**
* Set starting position for data label sorting animation.
* @private
*/
function setDataLabelStartPos(point, dataLabel, isNew, isInside, alignOptions) {
const chart = this.chart, inverted = chart.inverted, xAxis = this.xAxis, reversed = xAxis.reversed, labelCenter = ((inverted ? dataLabel.height : dataLabel.width) || 0) / 2, pointWidth = point.pointWidth, halfWidth = pointWidth ? pointWidth / 2 : 0;
dataLabel.startXPos = inverted ?
alignOptions.x :
(reversed ?
-labelCenter - halfWidth :
xAxis.width - labelCenter + halfWidth);
dataLabel.startYPos = inverted ?
(reversed ?
this.yAxis.height - labelCenter + halfWidth :
-labelCenter - halfWidth) : alignOptions.y;
// We need to handle visibility in case of sorting point outside plot
// area
if (!isInside) {
dataLabel
.attr({ opacity: 1 })
.animate({ opacity: 0 }, void 0, dataLabel.hide);
}
else if (dataLabel.visibility === 'hidden') {
dataLabel.show();
dataLabel
.attr({ opacity: 0 })
.animate({ opacity: 1 });
}
// Save start position on first render, but do not change position
if (!chart.hasRendered) {
return;
}
// Set start position
if (isNew) {
dataLabel.attr({ x: dataLabel.startXPos, y: dataLabel.startYPos });
}
dataLabel.placed = true;
}
})(DataLabel || (DataLabel = {}));
/* *
*
* Default Export
*
* */
export default DataLabel;
/* *
*
* API Declarations
*
* */
/**
* Callback JavaScript function to format the data label as a string. Note that
* if a `format` is defined, the format takes precedence and the formatter is
* ignored.
*
* @callback Highcharts.DataLabelsFormatterCallbackFunction
*
* @param {Highcharts.Point} this
* Data label context to format
*
* @param {Highcharts.DataLabelsOptions} options
* [API options](/highcharts/plotOptions.series.dataLabels) of the data label
*
* @return {number|string|null|undefined}
* Formatted data label text
*/
/**
* Values for handling data labels that flow outside the plot area.
*
* @typedef {"allow"|"justify"} Highcharts.DataLabelsOverflowValue
*/
''; // Keeps doclets above in JS file