123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431 |
- /**
- * (c) 2010-2019 Torstein Honsi
- *
- * License: www.highcharts.com/license
- */
- 'use strict';
- import H from './Globals.js';
- import './Utilities.js';
- import './Series.js';
- var addEvent = H.addEvent,
- arrayMax = H.arrayMax,
- defined = H.defined,
- extend = H.extend,
- format = H.format,
- merge = H.merge,
- noop = H.noop,
- pick = H.pick,
- relativeLength = H.relativeLength,
- Series = H.Series,
- seriesTypes = H.seriesTypes,
- stableSort = H.stableSort,
- isArray = H.isArray,
- splat = H.splat;
- /**
- * General distribution algorithm for distributing labels of differing size
- * along a confined length in two dimensions. The algorithm takes an array of
- * objects containing a size, a target and a rank. It will place the labels as
- * close as possible to their targets, skipping the lowest ranked labels if
- * necessary.
- *
- * @private
- * @function Highcharts.distribute
- *
- * @param {Array<object>} boxes
- *
- * @param {number} len
- *
- * @param {number} maxDistance
- */
- H.distribute = function (boxes, len, maxDistance) {
- var i,
- overlapping = true,
- origBoxes = boxes, // Original array will be altered with added .pos
- restBoxes = [], // The outranked overshoot
- box,
- target,
- total = 0,
- reducedLen = origBoxes.reducedLen || len;
- function sortByTarget(a, b) {
- return a.target - b.target;
- }
- // If the total size exceeds the len, remove those boxes with the lowest
- // rank
- i = boxes.length;
- while (i--) {
- total += boxes[i].size;
- }
- // Sort by rank, then slice away overshoot
- if (total > reducedLen) {
- stableSort(boxes, function (a, b) {
- return (b.rank || 0) - (a.rank || 0);
- });
- i = 0;
- total = 0;
- while (total <= reducedLen) {
- total += boxes[i].size;
- i++;
- }
- restBoxes = boxes.splice(i - 1, boxes.length);
- }
- // Order by target
- stableSort(boxes, sortByTarget);
- // So far we have been mutating the original array. Now
- // create a copy with target arrays
- boxes = boxes.map(function (box) {
- return {
- size: box.size,
- targets: [box.target],
- align: pick(box.align, 0.5)
- };
- });
- while (overlapping) {
- // Initial positions: target centered in box
- i = boxes.length;
- while (i--) {
- box = boxes[i];
- // Composite box, average of targets
- target = (
- Math.min.apply(0, box.targets) +
- Math.max.apply(0, box.targets)
- ) / 2;
- box.pos = Math.min(
- Math.max(0, target - box.size * box.align),
- len - box.size
- );
- }
- // Detect overlap and join boxes
- i = boxes.length;
- overlapping = false;
- while (i--) {
- // Overlap
- if (i > 0 && boxes[i - 1].pos + boxes[i - 1].size > boxes[i].pos) {
- // Add this size to the previous box
- boxes[i - 1].size += boxes[i].size;
- boxes[i - 1].targets = boxes[i - 1]
- .targets
- .concat(boxes[i].targets);
- boxes[i - 1].align = 0.5;
- // Overlapping right, push left
- if (boxes[i - 1].pos + boxes[i - 1].size > len) {
- boxes[i - 1].pos = len - boxes[i - 1].size;
- }
- boxes.splice(i, 1); // Remove this item
- overlapping = true;
- }
- }
- }
- // Add the rest (hidden boxes)
- origBoxes.push.apply(origBoxes, restBoxes);
- // Now the composite boxes are placed, we need to put the original boxes
- // within them
- i = 0;
- boxes.some(function (box) {
- var posInCompositeBox = 0;
- if (box.targets.some(function () {
- origBoxes[i].pos = box.pos + posInCompositeBox;
- // If the distance between the position and the target exceeds
- // maxDistance, abort the loop and decrease the length in increments
- // of 10% to recursively reduce the number of visible boxes by
- // rank. Once all boxes are within the maxDistance, we're good.
- if (
- Math.abs(origBoxes[i].pos - origBoxes[i].target) >
- maxDistance
- ) {
- // Reset the positions that are already set
- origBoxes.slice(0, i + 1).forEach(function (box) {
- delete box.pos;
- });
- // Try with a smaller length
- origBoxes.reducedLen =
- (origBoxes.reducedLen || len) - (len * 0.1);
- // Recurse
- if (origBoxes.reducedLen > len * 0.1) {
- H.distribute(origBoxes, len, maxDistance);
- }
- // Exceeded maxDistance => abort
- return true;
- }
- posInCompositeBox += origBoxes[i].size;
- i++;
- })) {
- // Exceeded maxDistance => abort
- return true;
- }
- });
- // Add the rest (hidden) boxes and sort by target
- stableSort(origBoxes, sortByTarget);
- };
- /**
- * Draw the data labels
- *
- * @private
- * @function Highcharts.Series#drawDataLabels
- *
- * @fires Highcharts.Series#event:afterDrawDataLabels
- */
- Series.prototype.drawDataLabels = function () {
- var series = this,
- chart = series.chart,
- seriesOptions = series.options,
- seriesDlOptions = seriesOptions.dataLabels,
- points = series.points,
- pointOptions,
- hasRendered = series.hasRendered || 0,
- dataLabelsGroup,
- defer = pick(seriesDlOptions.defer, !!seriesOptions.animation),
- renderer = chart.renderer;
- /*
- * Handle the dataLabels.filter option.
- */
- function applyFilter(point, options) {
- var filter = options.filter,
- op,
- prop,
- val;
- if (filter) {
- 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)
- ) {
- return true;
- }
- return false;
- }
- return true;
- }
- /*
- * 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.
- */
- function mergeArrays(one, two) {
- var 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 {
- i = Math.max(one.length, two.length);
- while (i--) {
- res[i] = merge(one[i], two[i]);
- }
- }
- return res;
- }
- // Merge in plotOptions.dataLabels for series
- seriesDlOptions = mergeArrays(
- mergeArrays(
- chart.options.plotOptions &&
- chart.options.plotOptions.series &&
- chart.options.plotOptions.series.dataLabels,
- chart.options.plotOptions &&
- chart.options.plotOptions[series.type] &&
- chart.options.plotOptions[series.type].dataLabels
- ),
- seriesDlOptions
- );
- H.fireEvent(this, 'drawDataLabels');
- if (
- isArray(seriesDlOptions) ||
- seriesDlOptions.enabled ||
- series._hasPointLabels
- ) {
- // Create a separate group for the data labels to avoid rotation
- dataLabelsGroup = series.plotGroup(
- 'dataLabelsGroup',
- 'data-labels',
- defer && !hasRendered ? 'hidden' : 'visible', // #5133
- seriesDlOptions.zIndex || 6
- );
- if (defer) {
- dataLabelsGroup.attr({ opacity: +hasRendered }); // #3300
- if (!hasRendered) {
- addEvent(series, 'afterAnimate', function () {
- if (series.visible) { // #2597, #3023, #3024
- dataLabelsGroup.show(true);
- }
- dataLabelsGroup[
- seriesOptions.animation ? 'animate' : 'attr'
- ]({ opacity: 1 }, { duration: 200 });
- });
- }
- }
- // Make the labels for each point
- points.forEach(function (point) {
- // 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,
- point.dlOptions || // dlOptions is used in treemaps
- (point.options && point.options.dataLabels)
- )
- );
- // Handle each individual data label for this point
- pointOptions.forEach(function (labelOptions, i) {
- // Options for one datalabel
- var labelEnabled = labelOptions.enabled &&
- !point.isNull && // #2282, #4641, #7112
- applyFilter(point, labelOptions),
- labelConfig,
- formatString,
- labelText,
- style,
- rotation,
- attr,
- dataLabel = point.dataLabels ? point.dataLabels[i] :
- point.dataLabel,
- connector = point.connectors ? point.connectors[i] :
- point.connector,
- isNew = !dataLabel;
- if (labelEnabled) {
- // Create individual options structure that can be extended
- // without affecting others
- labelConfig = point.getLabelConfig();
- formatString = (
- labelOptions[point.formatPrefix + 'Format'] ||
- labelOptions.format
- );
- labelText = defined(formatString) ?
- format(formatString, labelConfig, chart.time) :
- (
- labelOptions[point.formatPrefix + 'Formatter'] ||
- labelOptions.formatter
- ).call(labelConfig, labelOptions);
- style = labelOptions.style;
- rotation = labelOptions.rotation;
- if (!chart.styledMode) {
- // Determine the color
- style.color = pick(
- labelOptions.color,
- style.color,
- series.color,
- '#000000'
- );
- // Get automated contrast color
- if (style.color === 'contrast') {
- point.contrastColor = renderer.getContrast(
- point.color || series.color
- );
- style.color = labelOptions.inside ||
- pick(
- labelOptions.distance,
- point.labelDistance
- ) < 0 ||
- !!seriesOptions.stacking ?
- point.contrastColor :
- '#000000';
- }
- if (seriesOptions.cursor) {
- style.cursor = seriesOptions.cursor;
- }
- }
- attr = {
- r: labelOptions.borderRadius || 0,
- rotation: rotation,
- padding: labelOptions.padding,
- zIndex: 1
- };
- if (!chart.styledMode) {
- attr.fill = labelOptions.backgroundColor;
- attr.stroke = labelOptions.borderColor;
- attr['stroke-width'] = labelOptions.borderWidth;
- }
- // Remove unused attributes (#947)
- H.objectEach(attr, function (val, name) {
- if (val === undefined) {
- delete attr[name];
- }
- });
- }
- // If the point is outside the plot area, destroy it. #678, #820
- if (dataLabel && (!labelEnabled || !defined(labelText))) {
- point.dataLabel =
- point.dataLabel && point.dataLabel.destroy();
- if (point.dataLabels) {
- // Remove point.dataLabels if this was the last one
- if (point.dataLabels.length === 1) {
- delete point.dataLabels;
- } else {
- delete point.dataLabels[i];
- }
- }
- if (!i) {
- delete point.dataLabel;
- }
- if (connector) {
- point.connector = point.connector.destroy();
- if (point.connectors) {
- // Remove point.connectors if this was the last one
- if (point.connectors.length === 1) {
- delete point.connectors;
- } else {
- delete point.connectors[i];
- }
- }
- }
- // Individual labels are disabled if the are explicitly disabled
- // in the point options, or if they fall outside the plot area.
- } else if (labelEnabled && defined(labelText)) {
- if (!dataLabel) {
- // Create new label element
- point.dataLabels = point.dataLabels || [];
- dataLabel = point.dataLabels[i] = rotation ?
- // Labels don't rotate, use text element
- renderer.text(labelText, 0, -9999)
- .addClass('highcharts-data-label') :
- // We can use label
- renderer.label(
- labelText,
- 0,
- -9999,
- labelOptions.shape,
- null,
- null,
- labelOptions.useHTML,
- null,
- 'data-label'
- );
- // Store for backwards compatibility
- if (!i) {
- point.dataLabel = dataLabel;
- }
- 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
- 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);
- }
- 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, null, isNew
- );
- }
- });
- });
- }
- H.fireEvent(this, 'afterDrawDataLabels');
- };
- /**
- * Align each individual data label.
- *
- * @private
- * @function Highcharts.Series#alignDataLabel
- *
- * @param {Highcharts.Point} point
- *
- * @param {Highcharts.SVGElement} dataLabel
- *
- * @param {Highcharts.PlotSeriesDataLabelsOptions} options
- *
- * @param {Highcharts.BBoxObject} alignTo
- *
- * @param {boolean} isNew
- */
- Series.prototype.alignDataLabel = function (
- point,
- dataLabel,
- options,
- alignTo,
- isNew
- ) {
- var chart = this.chart,
- inverted = this.isCartesian && chart.inverted,
- plotX = pick(point.dlBox && point.dlBox.centerX, point.plotX, -9999),
- plotY = pick(point.plotY, -9999),
- bBox = dataLabel.getBBox(),
- baseline,
- rotation = options.rotation,
- normRotation,
- negRotation,
- align = options.align,
- rotCorr, // rotation correction
- // Math.round for rounding errors (#2683), alignTo to allow column
- // labels (#2700)
- visible =
- this.visible &&
- (
- point.series.forceDL ||
- chart.isInsidePlot(plotX, Math.round(plotY), inverted) ||
- (
- alignTo && chart.isInsidePlot(
- plotX,
- inverted ?
- alignTo.x + 1 :
- alignTo.y + alignTo.height - 1,
- inverted
- )
- )
- ),
- alignAttr, // the final position;
- justify = pick(options.overflow, 'justify') === 'justify';
- if (visible) {
- baseline = chart.renderer.fontMetrics(
- chart.styledMode ? undefined : options.style.fontSize,
- dataLabel
- ).b;
- // The alignment box is a singular point
- alignTo = extend({
- x: inverted ? this.yAxis.len - plotY : plotX,
- y: Math.round(inverted ? this.xAxis.len - plotX : plotY),
- width: 0,
- height: 0
- }, alignTo);
- // Add the text size for alignment calculation
- extend(options, {
- width: bBox.width,
- height: bBox.height
- });
- // Allow a hook for changing alignment in the last moment, then do the
- // alignment
- if (rotation) {
- justify = false; // Not supported for rotated text
- rotCorr = chart.renderer.rotCorr(baseline, rotation); // #3723
- alignAttr = {
- x: alignTo.x + options.x + alignTo.width / 2 + rotCorr.x,
- y: (
- alignTo.y +
- options.y +
- { top: 0, middle: 0.5, bottom: 1 }[options.verticalAlign] *
- alignTo.height
- )
- };
- dataLabel[isNew ? 'attr' : 'animate'](alignAttr)
- .attr({ // #3003
- align: align
- });
- // Compensate for the rotated label sticking out on the sides
- normRotation = (rotation + 720) % 360;
- negRotation = normRotation > 180 && normRotation < 360;
- if (align === 'left') {
- alignAttr.y -= negRotation ? bBox.height : 0;
- } else if (align === 'center') {
- alignAttr.x -= bBox.width / 2;
- alignAttr.y -= bBox.height / 2;
- } else if (align === 'right') {
- alignAttr.x -= bBox.width;
- alignAttr.y -= negRotation ? 0 : bBox.height;
- }
- dataLabel.placed = true;
- dataLabel.alignAttr = alignAttr;
- } else {
- dataLabel.align(options, null, alignTo);
- alignAttr = dataLabel.alignAttr;
- }
- // Handle justify or crop
- if (justify && alignTo.height >= 0) { // #8830
- point.isLabelJustified = this.justifyDataLabel(
- dataLabel,
- options,
- alignAttr,
- bBox,
- alignTo,
- isNew
- );
- // Now check that the data label is within the plot area
- } else if (pick(options.crop, true)) {
- visible =
- chart.isInsidePlot(
- alignAttr.x,
- alignAttr.y
- ) &&
- chart.isInsidePlot(
- alignAttr.x + bBox.width,
- alignAttr.y + bBox.height
- );
- }
- // When we're using a shape, make it possible with a connector or an
- // arrow pointing to thie point
- if (options.shape && !rotation) {
- dataLabel[isNew ? 'attr' : 'animate']({
- anchorX: inverted ? chart.plotWidth - point.plotY : point.plotX,
- anchorY: inverted ? chart.plotHeight - point.plotX : point.plotY
- });
- }
- }
- // Show or hide based on the final aligned position
- if (!visible) {
- dataLabel.attr({ y: -9999 });
- dataLabel.placed = false; // don't animate back in
- }
- };
- /**
- * If data labels fall partly outside the plot area, align them back in, in a
- * way that doesn't hide the point.
- *
- * @private
- * @function Highcharts.Series#justifyDataLabel
- *
- * @param {Highcharts.SVGElement} dataLabel
- *
- * @param {Highcharts.PlotSeriesDataLabelsOptions} options
- *
- * @param {*} alignAttr
- *
- * @param {Highcharts.BBoxObject} bBox
- *
- * @param {boolean} isNew
- *
- * @return {boolean}
- */
- Series.prototype.justifyDataLabel = function (
- dataLabel,
- options,
- alignAttr,
- bBox,
- alignTo,
- isNew
- ) {
- var chart = this.chart,
- align = options.align,
- verticalAlign = options.verticalAlign,
- off,
- justified,
- padding = dataLabel.box ? 0 : (dataLabel.padding || 0);
- // Off left
- off = alignAttr.x + padding;
- if (off < 0) {
- if (align === 'right') {
- options.align = 'left';
- } else {
- options.x = -off;
- }
- justified = true;
- }
- // Off right
- off = alignAttr.x + bBox.width - padding;
- if (off > chart.plotWidth) {
- if (align === 'left') {
- options.align = 'right';
- } else {
- options.x = chart.plotWidth - off;
- }
- justified = true;
- }
- // Off top
- off = alignAttr.y + padding;
- if (off < 0) {
- if (verticalAlign === 'bottom') {
- options.verticalAlign = 'top';
- } else {
- options.y = -off;
- }
- justified = true;
- }
- // Off bottom
- off = alignAttr.y + bBox.height - padding;
- if (off > chart.plotHeight) {
- if (verticalAlign === 'top') {
- options.verticalAlign = 'bottom';
- } else {
- options.y = chart.plotHeight - off;
- }
- justified = true;
- }
- if (justified) {
- dataLabel.placed = !isNew;
- dataLabel.align(options, null, alignTo);
- }
- return justified;
- };
- if (seriesTypes.pie) {
- seriesTypes.pie.prototype.dataLabelPositioners = {
- // Based on the value computed in Highcharts' distribute algorithm.
- radialDistributionY: function (point) {
- return point.top + point.distributeBox.pos;
- },
- // get the x - use the natural x position for labels near the
- // top and bottom, to prevent the top and botton slice
- // connectors from touching each other on either side
- // Based on the value computed in Highcharts' distribute algorithm.
- radialDistributionX: function (series, point, y, naturalY) {
- return series.getX(
- y < point.top + 2 || y > point.bottom - 2 ?
- naturalY :
- y,
- point.half,
- point
- );
- },
- // dataLabels.distance determines the x position of the label
- justify: function (point, radius, seriesCenter) {
- return seriesCenter[0] + (point.half ? -1 : 1) *
- (radius + point.labelDistance);
- },
- // Left edges of the left-half labels touch the left edge of the plot
- // area. Right edges of the right-half labels touch the right edge of
- // the plot area.
- alignToPlotEdges: function (
- dataLabel,
- half,
- plotWidth,
- plotLeft
- ) {
- var dataLabelWidth = dataLabel.getBBox().width;
- return half ? dataLabelWidth + plotLeft :
- plotWidth - dataLabelWidth - plotLeft;
- },
- // Connectors of each side end in the same x position. Labels are
- // aligned to them. Left edge of the widest left-half label touches the
- // left edge of the plot area. Right edge of the widest right-half label
- // touches the right edge of the plot area.
- alignToConnectors: function (
- points,
- half,
- plotWidth,
- plotLeft
- ) {
- var maxDataLabelWidth = 0,
- dataLabelWidth;
- // find widest data label
- points.forEach(function (point) {
- dataLabelWidth = point.dataLabel.getBBox().width;
- if (dataLabelWidth > maxDataLabelWidth) {
- maxDataLabelWidth = dataLabelWidth;
- }
- });
- return half ? maxDataLabelWidth + plotLeft :
- plotWidth - maxDataLabelWidth - plotLeft;
- }
- };
- /**
- * Override the base drawDataLabels method by pie specific functionality
- *
- * @private
- * @function Highcharts.seriesTypes.pie#drawDataLabels
- */
- seriesTypes.pie.prototype.drawDataLabels = function () {
- var series = this,
- data = series.data,
- point,
- chart = series.chart,
- options = series.options.dataLabels,
- connectorPadding = options.connectorPadding,
- connectorWidth = pick(options.connectorWidth, 1),
- plotWidth = chart.plotWidth,
- plotHeight = chart.plotHeight,
- plotLeft = chart.plotLeft,
- maxWidth = Math.round(chart.chartWidth / 3),
- connector,
- seriesCenter = series.center,
- radius = seriesCenter[2] / 2,
- centerY = seriesCenter[1],
- dataLabel,
- dataLabelWidth,
- // labelPos,
- labelPosition,
- labelHeight,
- // divide the points into right and left halves for anti collision
- halves = [
- [], // right
- [] // left
- ],
- x,
- y,
- visibility,
- j,
- overflow = [0, 0, 0, 0], // top, right, bottom, left
- dataLabelPositioners = series.dataLabelPositioners;
- // get out if not enabled
- if (!series.visible || (!options.enabled && !series._hasPointLabels)) {
- return;
- }
- // Reset all labels that have been shortened
- data.forEach(function (point) {
- if (point.dataLabel && point.visible && point.dataLabel.shortened) {
- point.dataLabel
- .attr({
- width: 'auto'
- }).css({
- width: 'auto',
- textOverflow: 'clip'
- });
- point.dataLabel.shortened = false;
- }
- });
- // run parent method
- Series.prototype.drawDataLabels.apply(series);
- data.forEach(function (point) {
- if (point.dataLabel) {
- if (point.visible) { // #407, #2510
- // Arrange points for detection collision
- halves[point.half].push(point);
- // Reset positions (#4905)
- point.dataLabel._pos = null;
- // Avoid long labels squeezing the pie size too far down
- if (
- !defined(options.style.width) &&
- !defined(
- point.options.dataLabels &&
- point.options.dataLabels.style &&
- point.options.dataLabels.style.width
- )
- ) {
- if (point.dataLabel.getBBox().width > maxWidth) {
- point.dataLabel.css({
- // Use a fraction of the maxWidth to avoid
- // wrapping close to the end of the string.
- width: maxWidth * 0.7
- });
- point.dataLabel.shortened = true;
- }
- }
- } else {
- point.dataLabel = point.dataLabel.destroy();
- // Workaround to make pies destroy multiple datalabels
- // correctly. This logic needs rewriting to support multiple
- // datalabels fully.
- if (point.dataLabels && point.dataLabels.length === 1) {
- delete point.dataLabels;
- }
- }
- }
- });
- /* Loop over the points in each half, starting from the top and bottom
- * of the pie to detect overlapping labels.
- */
- halves.forEach(function (points, i) {
- var top,
- bottom,
- length = points.length,
- positions = [],
- naturalY,
- sideOverflow,
- size,
- distributionLength;
- if (!length) {
- return;
- }
- // Sort by angle
- series.sortByAngle(points, i - 0.5);
- // Only do anti-collision when we have dataLabels outside the pie
- // and have connectors. (#856)
- if (series.maxLabelDistance > 0) {
- top = Math.max(
- 0,
- centerY - radius - series.maxLabelDistance
- );
- bottom = Math.min(
- centerY + radius + series.maxLabelDistance,
- chart.plotHeight
- );
- points.forEach(function (point) {
- // check if specific points' label is outside the pie
- if (point.labelDistance > 0 && point.dataLabel) {
- // point.top depends on point.labelDistance value
- // Used for calculation of y value in getX method
- point.top = Math.max(
- 0,
- centerY - radius - point.labelDistance
- );
- point.bottom = Math.min(
- centerY + radius + point.labelDistance,
- chart.plotHeight
- );
- size = point.dataLabel.getBBox().height || 21;
- // point.positionsIndex is needed for getting index of
- // parameter related to specific point inside positions
- // array - not every point is in positions array.
- point.distributeBox = {
- target: point.labelPosition.natural.y -
- point.top + size / 2,
- size: size,
- rank: point.y
- };
- positions.push(point.distributeBox);
- }
- });
- distributionLength = bottom + size - top;
- H.distribute(
- positions,
- distributionLength,
- distributionLength / 5
- );
- }
- // Now the used slots are sorted, fill them up sequentially
- for (j = 0; j < length; j++) {
- point = points[j];
- // labelPos = point.labelPos;
- labelPosition = point.labelPosition;
- dataLabel = point.dataLabel;
- visibility = point.visible === false ? 'hidden' : 'inherit';
- naturalY = labelPosition.natural.y;
- y = naturalY;
- if (positions && defined(point.distributeBox)) {
- if (point.distributeBox.pos === undefined) {
- visibility = 'hidden';
- } else {
- labelHeight = point.distributeBox.size;
- // Find label's y position
- y = dataLabelPositioners.radialDistributionY(point);
- }
- }
- // It is needed to delete point.positionIndex for
- // dynamically added points etc.
- delete point.positionIndex;
- // Find label's x position
- // justify is undocumented in the API - preserve support for it
- if (options.justify) {
- x = dataLabelPositioners.justify(point, radius,
- seriesCenter);
- } else {
- switch (options.alignTo) {
- case 'connectors':
- x = dataLabelPositioners.alignToConnectors(points,
- i, plotWidth, plotLeft);
- break;
- case 'plotEdges':
- x = dataLabelPositioners.alignToPlotEdges(dataLabel,
- i, plotWidth, plotLeft);
- break;
- default:
- x = dataLabelPositioners.radialDistributionX(series,
- point, y, naturalY);
- }
- }
- // Record the placement and visibility
- dataLabel._attr = {
- visibility: visibility,
- align: labelPosition.alignment
- };
- dataLabel._pos = {
- x: (
- x +
- options.x +
- ({
- left: connectorPadding,
- right: -connectorPadding
- }[labelPosition.alignment] || 0)
- ),
- // 10 is for the baseline (label vs text)
- y: y + options.y - 10
- };
- // labelPos.x = x;
- // labelPos.y = y;
- labelPosition.final.x = x;
- labelPosition.final.y = y;
- // Detect overflowing data labels
- if (pick(options.crop, true)) {
- dataLabelWidth = dataLabel.getBBox().width;
- sideOverflow = null;
- // Overflow left
- if (
- x - dataLabelWidth < connectorPadding &&
- i === 1 // left half
- ) {
- sideOverflow = Math.round(
- dataLabelWidth - x + connectorPadding
- );
- overflow[3] = Math.max(sideOverflow, overflow[3]);
- // Overflow right
- } else if (
- x + dataLabelWidth > plotWidth - connectorPadding &&
- i === 0 // right half
- ) {
- sideOverflow = Math.round(
- x + dataLabelWidth - plotWidth + connectorPadding
- );
- overflow[1] = Math.max(sideOverflow, overflow[1]);
- }
- // Overflow top
- if (y - labelHeight / 2 < 0) {
- overflow[0] = Math.max(
- Math.round(-y + labelHeight / 2),
- overflow[0]
- );
- // Overflow left
- } else if (y + labelHeight / 2 > plotHeight) {
- overflow[2] = Math.max(
- Math.round(y + labelHeight / 2 - plotHeight),
- overflow[2]
- );
- }
- dataLabel.sideOverflow = sideOverflow;
- }
- } // for each point
- }); // for each half
- // Do not apply the final placement and draw the connectors until we
- // have verified that labels are not spilling over.
- if (
- arrayMax(overflow) === 0 ||
- this.verifyDataLabelOverflow(overflow)
- ) {
- // Place the labels in the final position
- this.placeDataLabels();
- // Draw the connectors
- if (connectorWidth) {
- this.points.forEach(function (point) {
- var isNew;
- connector = point.connector;
- dataLabel = point.dataLabel;
- if (
- dataLabel &&
- dataLabel._pos &&
- point.visible &&
- point.labelDistance > 0
- ) {
- visibility = dataLabel._attr.visibility;
- isNew = !connector;
- if (isNew) {
- point.connector = connector = chart.renderer.path()
- .addClass(
- 'highcharts-data-label-connector ' +
- ' highcharts-color-' + point.colorIndex +
- (
- point.className ?
- ' ' + point.className :
- ''
- )
- )
- .add(series.dataLabelsGroup);
- if (!chart.styledMode) {
- connector.attr({
- 'stroke-width': connectorWidth,
- 'stroke': (
- options.connectorColor ||
- point.color ||
- '#666666'
- )
- });
- }
- }
- connector[isNew ? 'attr' : 'animate']({
- d: point.getConnectorPath()
- });
- connector.attr('visibility', visibility);
- } else if (connector) {
- point.connector = connector.destroy();
- }
- });
- }
- }
- };
- /**
- * Extendable method for getting the path of the connector between the data
- * label and the pie slice.
- *
- * @private
- * @function Highcharts.seriesTypes.pie#connectorPath
- *
- * @param {*} labelPos
- *
- * @return {Highcharts.PathObject}
- */
- // TODO: depracated - remove it
- /*
- seriesTypes.pie.prototype.connectorPath = function (labelPos) {
- var x = labelPos.x,
- y = labelPos.y;
- return pick(this.options.dataLabels.softConnector, true) ? [
- 'M',
- // end of the string at the label
- x + (labelPos[6] === 'left' ? 5 : -5), y,
- 'C',
- x, y, // first break, next to the label
- 2 * labelPos[2] - labelPos[4], 2 * labelPos[3] - labelPos[5],
- labelPos[2], labelPos[3], // second break
- 'L',
- labelPos[4], labelPos[5] // base
- ] : [
- 'M',
- // end of the string at the label
- x + (labelPos[6] === 'left' ? 5 : -5), y,
- 'L',
- labelPos[2], labelPos[3], // second break
- 'L',
- labelPos[4], labelPos[5] // base
- ];
- };
- */
- /**
- * Perform the final placement of the data labels after we have verified
- * that they fall within the plot area.
- *
- * @private
- * @function Highcharts.seriesTypes.pie#placeDataLabels
- */
- seriesTypes.pie.prototype.placeDataLabels = function () {
- this.points.forEach(function (point) {
- var dataLabel = point.dataLabel,
- _pos;
- if (dataLabel && point.visible) {
- _pos = dataLabel._pos;
- if (_pos) {
- // Shorten data labels with ellipsis if they still overflow
- // after the pie has reached minSize (#223).
- if (dataLabel.sideOverflow) {
- dataLabel._attr.width =
- dataLabel.getBBox().width - dataLabel.sideOverflow;
- dataLabel.css({
- width: dataLabel._attr.width + 'px',
- textOverflow: (
- (this.options.dataLabels.style || {})
- .textOverflow ||
- 'ellipsis'
- )
- });
- dataLabel.shortened = true;
- }
- dataLabel.attr(dataLabel._attr);
- dataLabel[dataLabel.moved ? 'animate' : 'attr'](_pos);
- dataLabel.moved = true;
- } else if (dataLabel) {
- dataLabel.attr({ y: -9999 });
- }
- }
- }, this);
- };
- seriesTypes.pie.prototype.alignDataLabel = noop;
- /**
- * Verify whether the data labels are allowed to draw, or we should run more
- * translation and data label positioning to keep them inside the plot area.
- * Returns true when data labels are ready to draw.
- *
- * @private
- * @function Highcharts.seriesTypes.pie#verifyDataLabelOverflow
- *
- * @param {boolean} overflow
- *
- * @return {boolean}
- */
- seriesTypes.pie.prototype.verifyDataLabelOverflow = function (overflow) {
- var center = this.center,
- options = this.options,
- centerOption = options.center,
- minSize = options.minSize || 80,
- newSize = minSize,
- // If a size is set, return true and don't try to shrink the pie
- // to fit the labels.
- ret = options.size !== null;
- if (!ret) {
- // Handle horizontal size and center
- if (centerOption[0] !== null) { // Fixed center
- newSize = Math.max(center[2] -
- Math.max(overflow[1], overflow[3]), minSize);
- } else { // Auto center
- newSize = Math.max(
- // horizontal overflow
- center[2] - overflow[1] - overflow[3],
- minSize
- );
- // horizontal center
- center[0] += (overflow[3] - overflow[1]) / 2;
- }
- // Handle vertical size and center
- if (centerOption[1] !== null) { // Fixed center
- newSize = Math.max(Math.min(newSize, center[2] -
- Math.max(overflow[0], overflow[2])), minSize);
- } else { // Auto center
- newSize = Math.max(
- Math.min(
- newSize,
- // vertical overflow
- center[2] - overflow[0] - overflow[2]
- ),
- minSize
- );
- // vertical center
- center[1] += (overflow[0] - overflow[2]) / 2;
- }
- // If the size must be decreased, we need to run translate and
- // drawDataLabels again
- if (newSize < center[2]) {
- center[2] = newSize;
- center[3] = Math.min( // #3632
- relativeLength(options.innerSize || 0, newSize),
- newSize
- );
- this.translate(center);
- if (this.drawDataLabels) {
- this.drawDataLabels();
- }
- // Else, return true to indicate that the pie and its labels is
- // within the plot area
- } else {
- ret = true;
- }
- }
- return ret;
- };
- }
- if (seriesTypes.column) {
- /**
- * Override the basic data label alignment by adjusting for the position of
- * the column.
- *
- * @private
- * @function Highcharts.seriesTypes.column#alignDataLabel
- *
- * @param {Highcharts.Point} point
- *
- * @param {Highcharts.SVGElement} dataLabel
- *
- * @param {Highcharts.PlotSeriesDataLabelsOptions} options
- *
- * @param {Highcharts.BBoxObject} alignTo
- *
- * @param {boolean} isNew
- */
- seriesTypes.column.prototype.alignDataLabel = function (
- point,
- dataLabel,
- options,
- alignTo,
- isNew
- ) {
- var inverted = this.chart.inverted,
- series = point.series,
- // data label box for alignment
- dlBox = point.dlBox || point.shapeArgs,
- below = pick(
- point.below, // range series
- point.plotY > pick(this.translatedThreshold, series.yAxis.len)
- ),
- // draw it inside the box?
- inside = pick(options.inside, !!this.options.stacking),
- overshoot;
- // Align to the column itself, or the top of it
- if (dlBox) { // Area range uses this method but not alignTo
- alignTo = merge(dlBox);
- if (alignTo.y < 0) {
- alignTo.height += alignTo.y;
- alignTo.y = 0;
- }
- overshoot = alignTo.y + alignTo.height - series.yAxis.len;
- if (overshoot > 0) {
- alignTo.height -= overshoot;
- }
- if (inverted) {
- alignTo = {
- x: series.yAxis.len - alignTo.y - alignTo.height,
- y: series.xAxis.len - alignTo.x - alignTo.width,
- width: alignTo.height,
- height: alignTo.width
- };
- }
- // Compute the alignment box
- if (!inside) {
- if (inverted) {
- alignTo.x += below ? 0 : alignTo.width;
- alignTo.width = 0;
- } else {
- alignTo.y += below ? alignTo.height : 0;
- alignTo.height = 0;
- }
- }
- }
- // When alignment is undefined (typically columns and bars), display the
- // individual point below or above the point depending on the threshold
- options.align = pick(
- options.align,
- !inverted || inside ? 'center' : below ? 'right' : 'left'
- );
- options.verticalAlign = pick(
- options.verticalAlign,
- inverted || inside ? 'middle' : below ? 'top' : 'bottom'
- );
- // Call the parent method
- Series.prototype.alignDataLabel.call(
- this,
- point,
- dataLabel,
- options,
- alignTo,
- isNew
- );
- // If label was justified and we have contrast, set it:
- if (point.isLabelJustified && point.contrastColor) {
- dataLabel.css({
- color: point.contrastColor
- });
- }
- };
- }
|