/* * * (c) 2010-2019 Highsoft AS * * Author: Paweł Potaczek * * License: www.highcharts.com/license */ /** * @interface Highcharts.LegendBubbleLegendFormatterContextObject *//** * The center y position of the range. * @name Highcharts.LegendBubbleLegendFormatterContextObject#center * @type {number} *//** * The radius of the bubble range. * @name Highcharts.LegendBubbleLegendFormatterContextObject#radius * @type {number} *//** * The bubble value. * @name Highcharts.LegendBubbleLegendFormatterContextObject#value * @type {number} */ 'use strict'; import H from '../parts/Globals.js'; var Series = H.Series, Legend = H.Legend, Chart = H.Chart, addEvent = H.addEvent, wrap = H.wrap, color = H.color, isNumber = H.isNumber, numberFormat = H.numberFormat, objectEach = H.objectEach, merge = H.merge, noop = H.noop, pick = H.pick, stableSort = H.stableSort, setOptions = H.setOptions, arrayMin = H.arrayMin, arrayMax = H.arrayMax; setOptions({ // Set default bubble legend options legend: { /** * The bubble legend is an additional element in legend which presents * the scale of the bubble series. Individual bubble ranges can be * defined by user or calculated from series. In the case of * automatically calculated ranges, a 1px margin of error is permitted. * Requires `highcharts-more.js`. * * @since 7.0.0 * @product highcharts highstock highmaps * @optionparent legend.bubbleLegend */ bubbleLegend: { /** * The color of the ranges borders, can be also defined for an * individual range. * * @sample highcharts/bubble-legend/similartoseries/ * Similat look to the bubble series * @sample highcharts/bubble-legend/bordercolor/ * Individual bubble border color * * @type {Highcharts.ColorString} */ borderColor: undefined, /** * The width of the ranges borders in pixels, can be also defined * for an individual range. */ borderWidth: 2, /** * An additional class name to apply to the bubble legend' circle * graphical elements. This option does not replace default class * names of the graphical element. * * @sample {highcharts} highcharts/css/bubble-legend/ * Styling by CSS * * @type {string} */ className: undefined, /** * The main color of the bubble legend. Applies to ranges, if * individual color is not defined. * * @sample highcharts/bubble-legend/similartoseries/ * Similat look to the bubble series * @sample highcharts/bubble-legend/color/ * Individual bubble color * * @type {Highcharts.ColorString|Highcharts.GradientColorObject|Highcharts.PatternObject} */ color: undefined, /** * An additional class name to apply to the bubble legend's * connector graphical elements. This option does not replace * default class names of the graphical element. * * @sample {highcharts} highcharts/css/bubble-legend/ * Styling by CSS * * @type {string} */ connectorClassName: undefined, /** * The color of the connector, can be also defined * for an individual range. * * @type {Highcharts.ColorString} */ connectorColor: undefined, /** * The length of the connectors in pixels. If labels are centered, * the distance is reduced to 0. * * @sample highcharts/bubble-legend/connectorandlabels/ * Increased connector length */ connectorDistance: 60, /** * The width of the connectors in pixels. * * @sample highcharts/bubble-legend/connectorandlabels/ * Increased connector width */ connectorWidth: 1, /** * Enable or disable the bubble legend. */ enabled: false, /** * Options for the bubble legend labels. */ labels: { /** * An additional class name to apply to the bubble legend * label graphical elements. This option does not replace * default class names of the graphical element. * * @sample {highcharts} highcharts/css/bubble-legend/ * Styling by CSS * * @type {string} */ className: undefined, /** * Whether to allow data labels to overlap. */ allowOverlap: false, /** * A [format string](http://docs.highcharts.com/#formatting) * for the bubble legend labels. Available variables are the * same as for `formatter`. * * @sample highcharts/bubble-legend/format/ * Add a unit * * @type {string} */ format: '', /** * Available `this` properties are: * * - `this.value`: The bubble value. * * - `this.radius`: The radius of the bubble range. * * - `this.center`: The center y position of the range. * * @type {Highcharts.FormatterCallbackFunction} */ formatter: undefined, /** * The alignment of the labels compared to the bubble legend. * Can be one of `left`, `center` or `right`. * @validvalue ["left", "center", "right"] * * @sample highcharts/bubble-legend/connectorandlabels/ * Labels on left * * @validvalue ["left", "center", "right"] */ align: 'right', /** * CSS styles for the labels. * * @type {Highcharts.CSSObject} */ style: { /** @ignore-option */ fontSize: 10, /** @ignore-option */ color: undefined }, /** * The x position offset of the label relative to the * connector. */ x: 0, /** * The y position offset of the label relative to the * connector. */ y: 0 }, /** * Miximum bubble legend range size. If values for ranges are not * specified, the `minSize` and the `maxSize` are calculated from * bubble series. */ maxSize: 60, // Number /** * Minimum bubble legend range size. If values for ranges are not * specified, the `minSize` and the `maxSize` are calculated from * bubble series. */ minSize: 10, // Number /** * The position of the bubble legend in the legend. * @sample highcharts/bubble-legend/connectorandlabels/ * Bubble legend as last item in legend */ legendIndex: 0, // Number /** * Options for specific range. One range consists of bubble, label * and connector. * * @sample highcharts/bubble-legend/ranges/ * Manually defined ranges * @sample highcharts/bubble-legend/autoranges/ * Auto calculated ranges * * @type {Array<*>} */ ranges: { /** * Range size value, similar to bubble Z data. */ value: undefined, /** * The color of the border for individual range. * @type {Highcharts.ColorString} */ borderColor: undefined, /** * The color of the bubble for individual range. * @type {Highcharts.ColorString|Highcharts.GradientColorObject|Highcharts.PatternObject} */ color: undefined, /** * The color of the connector for individual range. * @type {Highcharts.ColorString} */ connectorColor: undefined }, /** * Whether the bubble legend range value should be represented by * the area or the width of the bubble. The default, area, * corresponds best to the human perception of the size of each * bubble. * * @sample highcharts/bubble-legend/ranges/ * Size by width * * @validvalue ["area", "width"] */ sizeBy: 'area', /** * When this is true, the absolute value of z determines the size of * the bubble. This means that with the default zThreshold of 0, a * bubble of value -1 will have the same size as a bubble of value * 1, while a bubble of value 0 will have a smaller size according * to minSize. */ sizeByAbsoluteValue: false, /** * Define the visual z index of the bubble legend. */ zIndex: 1, /** * Ranges with with lower value than zThreshold, are skipped. */ zThreshold: 0 } } }); /** * BubbleLegend class. * * @private * @class * @name Highcharts.BubbleLegend * * @param {Highcharts.LegendBubbleLegendOptions} config * Bubble legend options * * @param {Highcharts.LegendOptions} config * Legend options */ H.BubbleLegend = function (options, legend) { this.init(options, legend); }; H.BubbleLegend.prototype = { /** * Create basic bubbleLegend properties similar to item in legend. * * @private * @function Highcharts.BubbleLegend#init * * @param {Highcharts.LegendBubbleLegendOptions} config * Bubble legend options * * @param {Highcharts.LegendOptions} config * Legend options */ init: function (options, legend) { this.options = options; this.visible = true; this.chart = legend.chart; this.legend = legend; }, setState: noop, /** * Depending on the position option, add bubbleLegend to legend items. * * @private * @function Highcharts.BubbleLegend#addToLegend * * @param {Array<*>} * All legend items */ addToLegend: function (items) { // Insert bubbleLegend into legend items items.splice(this.options.legendIndex, 0, this); }, /** * Calculate ranges, sizes and call the next steps of bubbleLegend creation. * * @private * @function Highcharts.BubbleLegend#drawLegendSymbol * * @param {Highcharts.Legend} legend * Legend instance */ drawLegendSymbol: function (legend) { var bubbleLegend = this, chart = bubbleLegend.chart, options = bubbleLegend.options, size, itemDistance = pick(legend.options.itemDistance, 20), connectorSpace, ranges = options.ranges, radius, maxLabel, connectorDistance = options.connectorDistance; // Predict label dimensions bubbleLegend.fontMetrics = chart.renderer.fontMetrics( options.labels.style.fontSize.toString() + 'px' ); // Do not create bubbleLegend now if ranges or ranges valeus are not // specified or if are empty array. if (!ranges || !ranges.length || !isNumber(ranges[0].value)) { legend.options.bubbleLegend.autoRanges = true; return; } // Sort ranges to right render order stableSort(ranges, function (a, b) { return b.value - a.value; }); bubbleLegend.ranges = ranges; bubbleLegend.setOptions(); bubbleLegend.render(); // Get max label size maxLabel = bubbleLegend.getMaxLabelSize(); radius = bubbleLegend.ranges[0].radius; size = radius * 2; // Space for connectors and labels. connectorSpace = connectorDistance - radius + maxLabel.width; connectorSpace = connectorSpace > 0 ? connectorSpace : 0; bubbleLegend.maxLabel = maxLabel; bubbleLegend.movementX = options.labels.align === 'left' ? connectorSpace : 0; bubbleLegend.legendItemWidth = size + connectorSpace + itemDistance; bubbleLegend.legendItemHeight = size + bubbleLegend.fontMetrics.h / 2; }, /** * Set style options for each bubbleLegend range. * * @private * @function Highcharts.BubbleLegend#setOptions */ setOptions: function () { var bubbleLegend = this, ranges = bubbleLegend.ranges, options = bubbleLegend.options, series = bubbleLegend.chart.series[options.seriesIndex], baseline = bubbleLegend.legend.baseline, bubbleStyle = { 'z-index': options.zIndex, 'stroke-width': options.borderWidth }, connectorStyle = { 'z-index': options.zIndex, 'stroke-width': options.connectorWidth }, labelStyle = bubbleLegend.getLabelStyles(), fillOpacity = series.options.marker.fillOpacity, styledMode = bubbleLegend.chart.styledMode; // Allow to parts of styles be used individually for range ranges.forEach(function (range, i) { if (!styledMode) { bubbleStyle.stroke = pick( range.borderColor, options.borderColor, series.color ); bubbleStyle.fill = pick( range.color, options.color, fillOpacity !== 1 ? color(series.color).setOpacity(fillOpacity) .get('rgba') : series.color ); connectorStyle.stroke = pick( range.connectorColor, options.connectorColor, series.color ); } // Set options needed for rendering each range ranges[i].radius = bubbleLegend.getRangeRadius(range.value); ranges[i] = merge(ranges[i], { center: ranges[0].radius - ranges[i].radius + baseline }); if (!styledMode) { merge(true, ranges[i], { bubbleStyle: merge(false, bubbleStyle), connectorStyle: merge(false, connectorStyle), labelStyle: labelStyle }); } }); }, /** * Merge options for bubbleLegend labels. * * @private * @function Highcharts.BubbleLegend#getLabelStyles */ getLabelStyles: function () { var options = this.options, additionalLabelsStyle = {}, labelsOnLeft = options.labels.align === 'left', rtl = this.legend.options.rtl; // To separate additional style options objectEach(options.labels.style, function (value, key) { if (key !== 'color' && key !== 'fontSize' && key !== 'z-index') { additionalLabelsStyle[key] = value; } }); return merge(false, additionalLabelsStyle, { 'font-size': options.labels.style.fontSize, fill: pick( options.labels.style.color, '#000000' ), 'z-index': options.zIndex, align: rtl || labelsOnLeft ? 'right' : 'left' }); }, /** * Calculate radius for each bubble range, * used code from BubbleSeries.js 'getRadius' method. * * @private * @function Highcharts.BubbleLegend#getRangeRadius * * @param {number} value * Range value * * @return {number} * Radius for one range */ getRangeRadius: function (value) { var bubbleLegend = this, options = bubbleLegend.options, seriesIndex = bubbleLegend.options.seriesIndex, bubbleSeries = bubbleLegend.chart.series[seriesIndex], zMax = options.ranges[0].value, zMin = options.ranges[options.ranges.length - 1].value, minSize = options.minSize, maxSize = options.maxSize; return bubbleSeries.getRadius.call( this, zMin, zMax, minSize, maxSize, value ); }, /** * Render the legendSymbol group. * * @private * @function Highcharts.BubbleLegend#render */ render: function () { var bubbleLegend = this, renderer = bubbleLegend.chart.renderer, zThreshold = bubbleLegend.options.zThreshold; if (!bubbleLegend.symbols) { bubbleLegend.symbols = { connectors: [], bubbleItems: [], labels: [] }; } // Nesting SVG groups to enable handleOverflow bubbleLegend.legendSymbol = renderer.g('bubble-legend'); bubbleLegend.legendItem = renderer.g('bubble-legend-item'); // To enable default 'hideOverlappingLabels' method bubbleLegend.legendSymbol.translateX = 0; bubbleLegend.legendSymbol.translateY = 0; bubbleLegend.ranges.forEach(function (range) { if (range.value >= zThreshold) { bubbleLegend.renderRange(range); } }); // To use handleOverflow method bubbleLegend.legendSymbol.add(bubbleLegend.legendItem); bubbleLegend.legendItem.add(bubbleLegend.legendGroup); bubbleLegend.hideOverlappingLabels(); }, /** * Render one range, consisting of bubble symbol, connector and label. * * @private * @function Highcharts.BubbleLegend#renderRange * * @param {Highcharts.LegendBubbleLegendRangesOptions} config * Range options * * @private */ renderRange: function (range) { var bubbleLegend = this, mainRange = bubbleLegend.ranges[0], legend = bubbleLegend.legend, options = bubbleLegend.options, labelsOptions = options.labels, chart = bubbleLegend.chart, renderer = chart.renderer, symbols = bubbleLegend.symbols, labels = symbols.labels, label, elementCenter = range.center, absoluteRadius = Math.abs(range.radius), connectorDistance = options.connectorDistance, labelsAlign = labelsOptions.align, rtl = legend.options.rtl, fontSize = labelsOptions.style.fontSize, connectorLength = rtl || labelsAlign === 'left' ? -connectorDistance : connectorDistance, borderWidth = options.borderWidth, connectorWidth = options.connectorWidth, posX = mainRange.radius, posY = elementCenter - absoluteRadius - borderWidth / 2 + connectorWidth / 2, labelY, labelX, fontMetrics = bubbleLegend.fontMetrics, labelMovement = fontSize / 2 - (fontMetrics.h - fontSize) / 2, crispMovement = (posY % 1 ? 1 : 0.5) - (connectorWidth % 2 ? 0 : 0.5), styledMode = renderer.styledMode; // Set options for centered labels if (labelsAlign === 'center') { connectorLength = 0; // do not use connector options.connectorDistance = 0; range.labelStyle.align = 'center'; } labelY = posY + options.labels.y; labelX = posX + connectorLength + options.labels.x; // Render bubble symbol symbols.bubbleItems.push( renderer .circle( posX, elementCenter + crispMovement, absoluteRadius ) .attr( styledMode ? {} : range.bubbleStyle ) .addClass( ( styledMode ? 'highcharts-color-' + bubbleLegend.options.seriesIndex + ' ' : '' ) + 'highcharts-bubble-legend-symbol ' + (options.className || '') ).add( bubbleLegend.legendSymbol ) ); // Render connector symbols.connectors.push( renderer .path(renderer.crispLine( ['M', posX, posY, 'L', posX + connectorLength, posY], options.connectorWidth )) .attr( styledMode ? {} : range.connectorStyle ) .addClass( ( styledMode ? 'highcharts-color-' + bubbleLegend.options.seriesIndex + ' ' : '' ) + 'highcharts-bubble-legend-connectors ' + (options.connectorClassName || '') ).add( bubbleLegend.legendSymbol ) ); // Render label label = renderer .text( bubbleLegend.formatLabel(range), labelX, labelY + labelMovement ) .attr( styledMode ? {} : range.labelStyle ) .addClass( 'highcharts-bubble-legend-labels ' + (options.labels.className || '') ).add( bubbleLegend.legendSymbol ); labels.push(label); // To enable default 'hideOverlappingLabels' method label.placed = true; label.alignAttr = { x: labelX, y: labelY + labelMovement }; }, /** * Get the label which takes up the most space. * * @private * @function Highcharts.BubbleLegend#getMaxLabelSize */ getMaxLabelSize: function () { var labels = this.symbols.labels, maxLabel, labelSize; labels.forEach(function (label) { labelSize = label.getBBox(true); if (maxLabel) { maxLabel = labelSize.width > maxLabel.width ? labelSize : maxLabel; } else { maxLabel = labelSize; } }); return maxLabel || {}; }, /** * Get formatted label for range. * * @private * @function Highcharts.BubbleLegend#formatLabel * * @param {Highcharts.LegendBubbleLegendRangesOptions} range * Range options * * @return {string} * Range label text */ formatLabel: function (range) { var options = this.options, formatter = options.labels.formatter, format = options.labels.format; return format ? H.format(format, range) : formatter ? formatter.call(range) : numberFormat(range.value, 1); }, /** * By using default chart 'hideOverlappingLabels' method, hide or show * labels and connectors. * * @private * @function Highcharts.BubbleLegend#hideOverlappingLabels */ hideOverlappingLabels: function () { var bubbleLegend = this, chart = this.chart, allowOverlap = bubbleLegend.options.labels.allowOverlap, symbols = bubbleLegend.symbols; if (!allowOverlap && symbols) { chart.hideOverlappingLabels(symbols.labels); // Hide or show connectors symbols.labels.forEach(function (label, index) { if (!label.newOpacity) { symbols.connectors[index].hide(); } else if (label.newOpacity !== label.oldOpacity) { symbols.connectors[index].show(); } }); } }, /** * Calculate ranges from created series. * * @private * @function Highcharts.BubbleLegend#getRanges * * @return {Array} * Array of range objects */ getRanges: function () { var bubbleLegend = this.legend.bubbleLegend, series = bubbleLegend.chart.series, ranges, rangesOptions = bubbleLegend.options.ranges, zData, minZ = Number.MAX_VALUE, maxZ = -Number.MAX_VALUE; series.forEach(function (s) { // Find the min and max Z, like in bubble series if (s.isBubble && !s.ignoreSeries) { zData = s.zData.filter(isNumber); if (zData.length) { minZ = pick(s.options.zMin, Math.min( minZ, Math.max( arrayMin(zData), s.options.displayNegative === false ? s.options.zThreshold : -Number.MAX_VALUE ) )); maxZ = pick( s.options.zMax, Math.max(maxZ, arrayMax(zData)) ); } } }); // Set values for ranges if (minZ === maxZ) { // Only one range if min and max values are the same. ranges = [{ value: maxZ }]; } else { ranges = [ { value: minZ }, { value: (minZ + maxZ) / 2 }, { value: maxZ, autoRanges: true } ]; } // Prevent reverse order of ranges after redraw if (rangesOptions.length && rangesOptions[0].radius) { ranges.reverse(); } // Merge ranges values with user options ranges.forEach(function (range, i) { if (rangesOptions && rangesOptions[i]) { ranges[i] = merge(false, rangesOptions[i], range); } }); return ranges; }, /** * Calculate bubble legend sizes from rendered series. * * @private * @function Highcharts.BubbleLegend#predictBubbleSizes * * @return {Array} * Calculated min and max bubble sizes */ predictBubbleSizes: function () { var chart = this.chart, fontMetrics = this.fontMetrics, legendOptions = chart.legend.options, floating = legendOptions.floating, horizontal = legendOptions.layout === 'horizontal', lastLineHeight = horizontal ? chart.legend.lastLineHeight : 0, plotSizeX = chart.plotSizeX, plotSizeY = chart.plotSizeY, bubbleSeries = chart.series[this.options.seriesIndex], minSize = Math.ceil(bubbleSeries.minPxSize), maxPxSize = Math.ceil(bubbleSeries.maxPxSize), maxSize = bubbleSeries.options.maxSize, plotSize = Math.min(plotSizeY, plotSizeX), calculatedSize; // Calculate prediceted max size of bubble if (floating || !(/%$/.test(maxSize))) { calculatedSize = maxPxSize; } else { maxSize = parseFloat(maxSize); calculatedSize = ((plotSize + lastLineHeight - fontMetrics.h / 2) * maxSize / 100) / (maxSize / 100 + 1); // Get maxPxSize from bubble series if calculated bubble legend // size will not affect to bubbles series. if ( (horizontal && plotSizeY - calculatedSize >= plotSizeX) || (!horizontal && plotSizeX - calculatedSize >= plotSizeY) ) { calculatedSize = maxPxSize; } } return [minSize, Math.ceil(calculatedSize)]; }, /** * Correct ranges with calculated sizes. * * @private * @function Highcharts.BubbleLegend#updateRanges * * @param {number} min * * @param {number} max */ updateRanges: function (min, max) { var bubbleLegendOptions = this.legend.options.bubbleLegend; bubbleLegendOptions.minSize = min; bubbleLegendOptions.maxSize = max; bubbleLegendOptions.ranges = this.getRanges(); }, /** * Because of the possibility of creating another legend line, predicted * bubble legend sizes may differ by a few pixels, so it is necessary to * correct them. * * @private * @function Highcharts.BubbleLegend#correctSizes */ correctSizes: function () { var legend = this.legend, chart = this.chart, bubbleSeries = chart.series[this.options.seriesIndex], bubbleSeriesSize = bubbleSeries.maxPxSize, bubbleLegendSize = this.options.maxSize; if (Math.abs(Math.ceil(bubbleSeriesSize) - bubbleLegendSize) > 1) { this.updateRanges(this.options.minSize, bubbleSeries.maxPxSize); legend.render(); } } }; // Start the bubble legend creation process. addEvent(H.Legend, 'afterGetAllItems', function (e) { var legend = this, bubbleLegend = legend.bubbleLegend, legendOptions = legend.options, options = legendOptions.bubbleLegend, bubbleSeriesIndex = legend.chart.getVisibleBubbleSeriesIndex(); // Remove unnecessary element if (bubbleLegend && bubbleLegend.ranges && bubbleLegend.ranges.length) { // Allow change the way of calculating ranges in update if (options.ranges.length) { options.autoRanges = !!options.ranges[0].autoRanges; } // Update bubbleLegend dimensions in each redraw legend.destroyItem(bubbleLegend); } // Create bubble legend if (bubbleSeriesIndex >= 0 && legendOptions.enabled && options.enabled ) { options.seriesIndex = bubbleSeriesIndex; legend.bubbleLegend = new H.BubbleLegend(options, legend); legend.bubbleLegend.addToLegend(e.allItems); } }); /** * Check if there is at least one visible bubble series. * * @private * @function Highcharts.Chart#getVisibleBubbleSeriesIndex * * @return {number} * First visible bubble series index */ Chart.prototype.getVisibleBubbleSeriesIndex = function () { var series = this.series, i = 0; while (i < series.length) { if ( series[i] && series[i].isBubble && series[i].visible && series[i].zData.length ) { return i; } i++; } return -1; }; /** * Calculate height for each row in legend. * * @private * @function Highcharts.Legend#getLinesHeights * * @return {Array} * Informations about line height and items amount */ Legend.prototype.getLinesHeights = function () { var items = this.allItems, lines = [], lastLine, length = items.length, i = 0, j = 0; for (i = 0; i < length; i++) { if (items[i].legendItemHeight) { // for bubbleLegend items[i].itemHeight = items[i].legendItemHeight; } if ( // Line break items[i] === items[length - 1] || items[i + 1] && items[i]._legendItemPos[1] !== items[i + 1]._legendItemPos[1] ) { lines.push({ height: 0 }); lastLine = lines[lines.length - 1]; // Find the highest item in line for (j; j <= i; j++) { if (items[j].itemHeight > lastLine.height) { lastLine.height = items[j].itemHeight; } } lastLine.step = i; } } return lines; }; /** * Correct legend items translation in case of different elements heights. * * @private * @function Highcharts.Legend#retranslateItems * * @param {Array} lines * Informations about line height and items amount */ Legend.prototype.retranslateItems = function (lines) { var items = this.allItems, orgTranslateX, orgTranslateY, movementX, rtl = this.options.rtl, actualLine = 0; items.forEach(function (item, index) { orgTranslateX = item.legendGroup.translateX; orgTranslateY = item._legendItemPos[1]; movementX = item.movementX; if (movementX || (rtl && item.ranges)) { movementX = rtl ? orgTranslateX - item.options.maxSize / 2 : orgTranslateX + movementX; item.legendGroup.attr({ translateX: movementX }); } if (index > lines[actualLine].step) { actualLine++; } item.legendGroup.attr({ translateY: Math.round( orgTranslateY + lines[actualLine].height / 2 ) }); item._legendItemPos[1] = orgTranslateY + lines[actualLine].height / 2; }); }; // Hide or show bubble legend depending on the visible status of bubble series. addEvent(Series, 'legendItemClick', function () { var series = this, chart = series.chart, visible = series.visible, legend = series.chart.legend, status; if (legend && legend.bubbleLegend) { // Visible property is not set correctly yet, so temporary correct it series.visible = !visible; // Save future status for getRanges method series.ignoreSeries = visible; // Check if at lest one bubble series is visible status = chart.getVisibleBubbleSeriesIndex() >= 0; // Hide bubble legend if all bubble series are disabled if (legend.bubbleLegend.visible !== status) { // Show or hide bubble legend legend.update({ bubbleLegend: { enabled: status } }); legend.bubbleLegend.visible = status; // Restore default status } series.visible = visible; } }); // If ranges are not specified, determine ranges from rendered bubble series and // render legend again. wrap(Chart.prototype, 'drawChartBox', function (proceed, options, callback) { var chart = this, legend = chart.legend, bubbleSeries = chart.getVisibleBubbleSeriesIndex() >= 0, bubbleLegendOptions, bubbleSizes; if ( legend && legend.options.enabled && legend.bubbleLegend && legend.options.bubbleLegend.autoRanges && bubbleSeries ) { bubbleLegendOptions = legend.bubbleLegend.options; bubbleSizes = legend.bubbleLegend.predictBubbleSizes(); legend.bubbleLegend.updateRanges(bubbleSizes[0], bubbleSizes[1]); // Disable animation on init if (!bubbleLegendOptions.placed) { legend.group.placed = false; legend.allItems.forEach(function (item) { item.legendGroup.translateY = null; }); } // Create legend with bubbleLegend legend.render(); chart.getMargins(); chart.axes.forEach(function (axis) { axis.render(); if (!bubbleLegendOptions.placed) { axis.setScale(); axis.updateNames(); // Disable axis animation on init objectEach(axis.ticks, function (tick) { tick.isNew = true; tick.isNewLabel = true; }); } }); bubbleLegendOptions.placed = true; // After recalculate axes, calculate margins again. chart.getMargins(); // Call default 'drawChartBox' method. proceed.call(chart, options, callback); // Check bubble legend sizes and correct them if necessary. legend.bubbleLegend.correctSizes(); // Correct items positions with different dimensions in legend. legend.retranslateItems(legend.getLinesHeights()); } else { proceed.call(chart, options, callback); if (legend && legend.options.enabled && legend.bubbleLegend) { // Allow color change after click in legend on static bubble legend legend.render(); legend.retranslateItems(legend.getLinesHeights()); } } });