UNPKG

highcharts

Version:
278 lines (277 loc) 10.2 kB
/* * * * (c) 2010-2025 Torstein Honsi * * License: www.highcharts.com/license * * !!!!!!! SOURCE GETS TRANSPILED BY TYPESCRIPT. EDIT TS FILE ONLY. !!!!!!! * * */ 'use strict'; import T from '../../Templating.js'; const { format } = T; import SeriesRegistry from '../../Series/SeriesRegistry.js'; const { series: Series } = SeriesRegistry; import U from '../../Utilities.js'; const { destroyObjectProperties, fireEvent, getAlignFactor, isNumber, pick } = U; /* * * * Class * * */ /** * The class for stacks. Each stack, on a specific X value and either negative * or positive, has its own stack item. * @private */ class StackItem { /* * * * Constructor * * */ constructor(axis, options, negativeValue, x, stackOption) { const inverted = axis.chart.inverted, reversed = axis.reversed; this.axis = axis; // The stack goes to the left either if the stack has negative value // or when axis is reversed. XOR operator. const isNegative = (this.isNegative = !!negativeValue !== !!reversed); // Save the options to be able to style the label this.options = options = options || {}; // Save the x value to be able to position the label later this.x = x; // Initialize total value this.total = null; this.cumulative = null; // This will keep each points' extremes stored by series.index and point // index this.points = {}; this.hasValidPoints = false; // Save the stack option on the series configuration object, // and whether to treat it as percent this.stack = stackOption; this.leftCliff = 0; this.rightCliff = 0; // The align options and text align varies on whether the stack is // negative and if the chart is inverted or not. // First test the user supplied value, then use the dynamic. this.alignOptions = { align: options.align || (inverted ? (isNegative ? 'left' : 'right') : 'center'), verticalAlign: options.verticalAlign || (inverted ? 'middle' : isNegative ? 'bottom' : 'top'), y: options.y, x: options.x }; this.textAlign = options.textAlign || (inverted ? (!isNegative ? 'left' : 'right') : 'center'); } /* * * * Functions * * */ /** * @private */ destroy() { destroyObjectProperties(this, this.axis); } /** * Renders the stack total label and adds it to the stack label group. * @private */ render(group) { const chart = this.axis.chart, options = this.options, formatOption = options.format, // Format the text in the label. str = formatOption ? format(formatOption, this, chart) : options.formatter.call(this); // Change the text to reflect the new total and set visibility to hidden // in case the series is hidden if (this.label) { this.label.attr({ text: str, visibility: 'hidden' }); } else { // Create new label this.label = chart.renderer.label(str, null, void 0, options.shape, void 0, void 0, options.useHTML, false, 'stack-labels'); const attr = { r: options.borderRadius || 0, text: str, // Set default padding to 5 as it is in datalabels #12308 padding: pick(options.padding, 5), visibility: 'hidden' // Hidden until setOffset is called }; if (!chart.styledMode) { attr.fill = options.backgroundColor; attr.stroke = options.borderColor; attr['stroke-width'] = options.borderWidth; this.label.css(options.style || {}); } this.label.attr(attr); if (!this.label.added) { this.label.add(group); // Add to the labels-group } } // Rank it higher than data labels (#8742) this.label.labelrank = chart.plotSizeY; fireEvent(this, 'afterRender'); } /** * Sets the offset that the stack has from the x value and repositions the * label. * @private */ setOffset(xOffset, width, boxBottom, boxTop, defaultX, xAxis) { const { alignOptions, axis, label, options, textAlign } = this, chart = axis.chart, stackBox = this.getStackBox({ xOffset, width, boxBottom, boxTop, defaultX, xAxis }), { verticalAlign } = alignOptions; if (label && stackBox) { const labelBox = label.getBBox(void 0, 0), padding = label.padding; let isJustify = pick(options.overflow, 'justify') === 'justify', visible; // Reset alignOptions property after justify #12337 alignOptions.x = options.x || 0; alignOptions.y = options.y || 0; // Calculate the adjusted Stack position, to take into consideration // The size if the labelBox and vertical alignment as // well as the text alignment. It's need to be done to work with // default SVGLabel.align/justify methods. const { x, y } = this.adjustStackPosition({ labelBox, verticalAlign, textAlign }); stackBox.x -= x; stackBox.y -= y; // Align the label to the adjusted box. label.align(alignOptions, false, stackBox); // Check if label is inside the plotArea #12294 visible = chart.isInsidePlot(label.alignAttr.x + alignOptions.x + x, label.alignAttr.y + alignOptions.y + y); if (!visible) { isJustify = false; } if (isJustify) { // Justify stackLabel into the alignBox Series.prototype.justifyDataLabel.call(axis, label, alignOptions, label.alignAttr, labelBox, stackBox); } // Add attr to avoid the default animation of justifyDataLabel. // Also add correct rotation with its rotation origin. #15129 label.attr({ x: label.alignAttr.x, y: label.alignAttr.y, rotation: options.rotation, rotationOriginX: labelBox.width * getAlignFactor(options.textAlign || 'center'), rotationOriginY: labelBox.height / 2 }); // Check if the dataLabel should be visible. if (pick(!isJustify && options.crop, true)) { visible = isNumber(label.x) && isNumber(label.y) && chart.isInsidePlot(label.x - padding + (label.width || 0), label.y) && chart.isInsidePlot(label.x + padding, label.y); } label[visible ? 'show' : 'hide'](); } fireEvent(this, 'afterSetOffset', { xOffset, width }); } /** * Adjust the stack BBox position, to take into consideration the alignment * of the dataLabel. This is necessary to make the stackDataLabel work with * core methods like `SVGLabel.adjust` and `Series.justifyDataLabel`. * @param AdjustStackPositionProps * @return {{x: number, y: number}} Adjusted BBox position of the stack. */ adjustStackPosition({ labelBox, verticalAlign, textAlign }) { return { x: labelBox.width / 2 + (labelBox.width / 2) * (2 * getAlignFactor(textAlign) - 1), y: (labelBox.height / 2) * 2 * (1 - getAlignFactor(verticalAlign)) }; } /** * Get the bbox of the stack. * @private * @function Highcharts.StackItem#getStackBox * @return {BBoxObject} The x, y, height, width of the stack. */ getStackBox(stackBoxProps) { const stackItem = this, axis = this.axis, chart = axis.chart, { boxTop, defaultX, xOffset, width, boxBottom } = stackBoxProps, totalStackValue = axis.stacking.usePercentage ? 100 : pick(boxTop, this.total, 0), y = axis.toPixels(totalStackValue), xAxis = stackBoxProps.xAxis || chart.xAxis[0], x = pick(defaultX, xAxis.translate(this.x)) + xOffset, yZero = axis.toPixels(boxBottom || (isNumber(axis.min) && axis.logarithmic && axis.logarithmic.lin2log(axis.min)) || 0), height = Math.abs(y - yZero), inverted = chart.inverted, neg = stackItem.isNegative; return inverted ? { x: (neg ? y : y - height) - chart.plotLeft, y: xAxis.height - x - width + xAxis.top - chart.plotTop, width: height, height: width } : { x: x + xAxis.transB - chart.plotLeft, y: (neg ? y - height : y) - chart.plotTop, width: width, height: height }; } } /* * * * Default Export * * */ export default StackItem; /* * * * API Declarations * * */ /** * Stack of data points * * @product highcharts * * @interface Highcharts.StackItemObject */ /** * Alignment settings * @name Highcharts.StackItemObject#alignOptions * @type {Highcharts.AlignObject} */ /** * Related axis * @name Highcharts.StackItemObject#axis * @type {Highcharts.Axis} */ /** * Cumulative value of the stacked data points * @name Highcharts.StackItemObject#cumulative * @type {number} */ /** * True if on the negative side * @name Highcharts.StackItemObject#isNegative * @type {boolean} */ /** * Related SVG element * @name Highcharts.StackItemObject#label * @type {Highcharts.SVGElement} */ /** * Related stack options * @name Highcharts.StackItemObject#options * @type {Highcharts.YAxisStackLabelsOptions} */ /** * Total value of the stacked data points * @name Highcharts.StackItemObject#total * @type {number} */ /** * Shared x value of the stack * @name Highcharts.StackItemObject#x * @type {number} */ ''; // Keeps doclets above in JS file