123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567 |
- /* *
- * (c) 2010-2019 Grzegorz Blachlinski, Sebastian Bochan
- *
- * License: www.highcharts.com/license
- */
- 'use strict';
- import H from '../parts/Globals.js';
- import '../parts/Utilities.js';
- import '../parts/Axis.js';
- import '../parts/Color.js';
- import '../parts/Point.js';
- import '../parts/Series.js';
- var seriesType = H.seriesType,
- defined = H.defined;
- /**
- * Packed bubble series
- *
- * @private
- * @class
- * @name Highcharts.seriesTypes.packedbubble
- *
- * @augments Highcharts.Series
- *
- * @requires modules:highcharts-more
- */
- seriesType(
- 'packedbubble',
- 'bubble',
- /**
- * A packed bubble series is a two dimensional series type, where each point
- * renders a value in X, Y position. Each point is drawn as a bubble where
- * the bubbles don't overlap with each other and the radius of the bubble
- * related to the value. Requires `highcharts-more.js`.
- *
- * @sample {highcharts} highcharts/demo/packed-bubble/
- * Packed-bubble chart
- *
- * @extends plotOptions.bubble
- * @since 7.0.0
- * @product highcharts
- * @excluding connectEnds, connectNulls, jitter, keys,
- * sizeByAbsoluteValue, step, zMax, zMin
- * @optionparent plotOptions.packedbubble
- */
- {
- /**
- * Minimum bubble size. Bubbles will automatically size between the
- * `minSize` and `maxSize` to reflect the value of each bubble.
- * Can be either pixels (when no unit is given), or a percentage of
- * the smallest one of the plot width and height.
- *
- * @sample {highcharts} highcharts/plotoptions/bubble-size/
- * Bubble size
- *
- * @type {number|string}
- */
- minSize: '10%',
- /**
- * Maximum bubble size. Bubbles will automatically size between the
- * `minSize` and `maxSize` to reflect the value of each bubble.
- * Can be either pixels (when no unit is given), or a percentage of
- * the smallest one of the plot width and height.
- *
- * @sample {highcharts} highcharts/plotoptions/bubble-size/
- * Bubble size
- *
- * @type {number|string}
- */
- maxSize: '100%',
- sizeBy: 'radius',
- zoneAxis: 'y',
- tooltip: {
- pointFormat: 'Value: {point.value}'
- }
- }, {
- pointArrayMap: ['value'],
- pointValKey: 'value',
- isCartesian: false,
- axisTypes: [],
- /**
- * Create a single array of all points from all series
- * @private
- * @param {Array} Array of all series objects
- * @return {Array} Returns the array of all points.
- */
- accumulateAllPoints: function (series) {
- var chart = series.chart,
- allDataPoints = [],
- i, j;
- for (i = 0; i < chart.series.length; i++) {
- series = chart.series[i];
- if (series.visible || !chart.options.chart.ignoreHiddenSeries) {
- // add data to array only if series is visible
- for (j = 0; j < series.yData.length; j++) {
- allDataPoints.push([
- null, null,
- series.yData[j],
- series.index,
- j
- ]);
- }
- }
- }
- return allDataPoints;
- },
- // Extend the base translate method to handle bubble size, and correct
- // positioning them.
- translate: function () {
- var positions, // calculated positions of bubbles in bubble array
- series = this,
- chart = series.chart,
- data = series.data,
- index = series.index,
- point,
- radius,
- i;
- this.processedXData = this.xData;
- this.generatePoints();
- // merged data is an array with all of the data from all series
- if (!defined(chart.allDataPoints)) {
- chart.allDataPoints = series.accumulateAllPoints(series);
- // calculate radius for all added data
- series.getPointRadius();
- }
- // after getting initial radius, calculate bubble positions
- positions = this.placeBubbles(chart.allDataPoints);
- // Set the shape type and arguments to be picked up in drawPoints
- for (i = 0; i < positions.length; i++) {
- if (positions[i][3] === index) {
- // update the series points with the values from positions
- // array
- point = data[positions[i][4]];
- radius = positions[i][2];
- point.plotX = positions[i][0] - chart.plotLeft +
- chart.diffX;
- point.plotY = positions[i][1] - chart.plotTop +
- chart.diffY;
- point.marker = H.extend(point.marker, {
- radius: radius,
- width: 2 * radius,
- height: 2 * radius
- });
- }
- }
- },
- /**
- * Check if two bubbles overlaps.
- * @private
- * @param {Array} bubble1 first bubble
- * @param {Array} bubble2 second bubble
- * @return {boolean} overlap or not
- */
- checkOverlap: function (bubble1, bubble2) {
- var diffX = bubble1[0] - bubble2[0], // diff of X center values
- diffY = bubble1[1] - bubble2[1], // diff of Y center values
- sumRad = bubble1[2] + bubble2[2]; // sum of bubble radius
- return (
- Math.sqrt(diffX * diffX + diffY * diffY) -
- Math.abs(sumRad)
- ) < -0.001;
- },
- /**
- * Function that is adding one bubble based on positions and sizes
- * of two other bubbles, lastBubble is the last added bubble,
- * newOrigin is the bubble for positioning new bubbles.
- * nextBubble is the curently added bubble for which we are
- * calculating positions
- * @private
- * @param {Array} lastBubble The closest last bubble
- * @param {Array} newOrigin New bubble
- * @param {Array} nextBubble The closest next bubble
- * @return {Array} Bubble with correct positions
- */
- positionBubble: function (lastBubble, newOrigin, nextBubble) {
- var sqrt = Math.sqrt,
- asin = Math.asin,
- acos = Math.acos,
- pow = Math.pow,
- abs = Math.abs,
- distance = sqrt( // dist between lastBubble and newOrigin
- pow((lastBubble[0] - newOrigin[0]), 2) +
- pow((lastBubble[1] - newOrigin[1]), 2)
- ),
- alfa = acos(
- // from cosinus theorem: alfa is an angle used for
- // calculating correct position
- (
- pow(distance, 2) +
- pow(nextBubble[2] + newOrigin[2], 2) -
- pow(nextBubble[2] + lastBubble[2], 2)
- ) / (2 * (nextBubble[2] + newOrigin[2]) * distance)
- ),
- beta = asin( // from sinus theorem.
- abs(lastBubble[0] - newOrigin[0]) /
- distance
- ),
- // providing helping variables, related to angle between
- // lastBubble and newOrigin
- gamma = (lastBubble[1] - newOrigin[1]) < 0 ? 0 : Math.PI,
- // if new origin y is smaller than last bubble y value
- // (2 and 3 quarter),
- // add Math.PI to final angle
- delta = (lastBubble[0] - newOrigin[0]) *
- (lastBubble[1] - newOrigin[1]) < 0 ?
- 1 : -1, // (1st and 3rd quarter)
- finalAngle = gamma + alfa + beta * delta,
- cosA = Math.cos(finalAngle),
- sinA = Math.sin(finalAngle),
- posX = newOrigin[0] + (newOrigin[2] + nextBubble[2]) * sinA,
- // center of new origin + (radius1 + radius2) * sinus A
- posY = newOrigin[1] - (newOrigin[2] + nextBubble[2]) * cosA;
- return [
- posX,
- posY,
- nextBubble[2],
- nextBubble[3],
- nextBubble[4]
- ]; // the same as described before
- },
- /**
- * This is the main function responsible for positioning all of the
- * bubbles.
- * allDataPoints - bubble array, in format [pixel x value,
- * pixel y value, radius, related series index, related point index]
- * @private
- * @param {Array} allDataPoints All points from all series
- * @return {Array} Positions of all bubbles
- */
- placeBubbles: function (allDataPoints) {
- var series = this,
- checkOverlap = series.checkOverlap,
- positionBubble = series.positionBubble,
- bubblePos = [],
- stage = 1,
- j = 0,
- k = 0,
- calculatedBubble,
- sortedArr,
- i;
- // sort all points
- sortedArr = allDataPoints.sort(function (a, b) {
- return b[2] - a[2];
- });
- // if length is 0, return empty array
- if (!sortedArr.length) {
- return [];
- }
- if (sortedArr.length < 2) {
- // if length is 1,return only one bubble
- return [
- 0, 0,
- sortedArr[0][0],
- sortedArr[0][1],
- sortedArr[0][2]
- ];
- }
- // create first bubble in the middle of the chart
- bubblePos.push([
- [
- 0, // starting in 0,0 coordinates
- 0,
- sortedArr[0][2], // radius
- sortedArr[0][3], // series index
- sortedArr[0][4]
- ] // point index
- ]); // 0 level bubble
- bubblePos.push([
- [
- 0,
- 0 - sortedArr[1][2] - sortedArr[0][2],
- // move bubble above first one
- sortedArr[1][2],
- sortedArr[1][3],
- sortedArr[1][4]
- ]
- ]); // 1 level 1st bubble
- // first two already positioned so starting from 2
- for (i = 2; i < sortedArr.length; i++) {
- sortedArr[i][2] = sortedArr[i][2] || 1;
- // in case if radius is calculated as 0.
- calculatedBubble = positionBubble(
- bubblePos[stage][j],
- bubblePos[stage - 1][k],
- sortedArr[i]
- ); // calculate initial bubble position
- if (checkOverlap(calculatedBubble, bubblePos[stage][0])) {
- // if new bubble is overlapping with first bubble in
- // current level (stage)
- bubblePos.push([]);
- k = 0;
- // reset index of bubble, used for positioning the bubbles
- // around it, we are starting from first bubble in next
- // stage because we are changing level to higher
- bubblePos[stage + 1].push(
- positionBubble(
- bubblePos[stage][j],
- bubblePos[stage][0],
- sortedArr[i]
- )
- );
- // (last added bubble, 1st. bbl from cur stage, new bubble)
- stage++; // the new level is created, above current one
- j = 0; // set the index of bubble in current level to 0
- } else if (
- stage > 1 && bubblePos[stage - 1][k + 1] &&
- checkOverlap(calculatedBubble, bubblePos[stage - 1][k + 1])
- ) {
- // If new bubble is overlapping with one of the previous
- // stage bubbles, it means that - bubble, used for
- // positioning the bubbles around it has changed so we need
- // to recalculate it.
- k++;
- bubblePos[stage].push(
- positionBubble(
- bubblePos[stage][j],
- bubblePos[stage - 1][k],
- sortedArr[i]
- )
- );
- // (last added bubble, previous stage bubble, new bubble)
- j++;
- } else { // simply add calculated bubble
- j++;
- bubblePos[stage].push(calculatedBubble);
- }
- }
- series.chart.stages = bubblePos;
- // it may not be necessary but adding it just in case -
- // it is containing all of the bubble levels
- series.chart.rawPositions = [].concat.apply([], bubblePos);
- // bubble positions merged into one array
- series.resizeRadius();
- return series.chart.rawPositions;
- },
- /**
- * The function responsible for resizing the bubble radius.
- * In shortcut: it is taking the initially
- * calculated positions of bubbles. Then it is calculating the min max
- * of both dimensions, creating something in shape of bBox.
- * The comparison of bBox and the size of plotArea
- * (later it may be also the size set by customer) is giving the
- * value how to recalculate the radius so it will match the size
- * @private
- */
- resizeRadius: function () {
- var chart = this.chart,
- positions = chart.rawPositions,
- min = Math.min,
- max = Math.max,
- plotLeft = chart.plotLeft,
- plotTop = chart.plotTop,
- chartHeight = chart.plotHeight,
- chartWidth = chart.plotWidth,
- minX, maxX, minY, maxY,
- radius,
- bBox,
- spaceRatio,
- smallerDimension,
- i;
- minX = minY = Number.POSITIVE_INFINITY; // set initial values
- maxX = maxY = Number.NEGATIVE_INFINITY;
- for (i = 0; i < positions.length; i++) {
- radius = positions[i][2];
- minX = min(minX, positions[i][0] - radius);
- // (x center-radius) is the min x value used by specific bubble
- maxX = max(maxX, positions[i][0] + radius);
- minY = min(minY, positions[i][1] - radius);
- maxY = max(maxY, positions[i][1] + radius);
- }
- bBox = [maxX - minX, maxY - minY];
- spaceRatio = [
- (chartWidth - plotLeft) / bBox[0],
- (chartHeight - plotTop) / bBox[1]
- ];
- smallerDimension = min.apply([], spaceRatio);
- if (Math.abs(smallerDimension - 1) > 1e-10) {
- // if bBox is considered not the same width as possible size
- for (i = 0; i < positions.length; i++) {
- positions[i][2] *= smallerDimension;
- }
- this.placeBubbles(positions);
- } else {
- // If no radius recalculation is needed, we need to position the
- // whole bubbles in center of chart plotarea for this, we are
- // adding two parameters, diffY and diffX, that are related to
- // differences between the initial center and the bounding box.
- chart.diffY = chartHeight / 2 +
- plotTop - minY - (maxY - minY) / 2;
- chart.diffX = chartWidth / 2 +
- plotLeft - minX - (maxX - minX) / 2;
- }
- },
- // Calculate radius of bubbles in series.
- getPointRadius: function () { // bubbles array
- var series = this,
- chart = series.chart,
- plotWidth = chart.plotWidth,
- plotHeight = chart.plotHeight,
- seriesOptions = series.options,
- smallestSize = Math.min(plotWidth, plotHeight),
- extremes = {},
- radii = [],
- allDataPoints = chart.allDataPoints,
- minSize,
- maxSize,
- value,
- radius;
- ['minSize', 'maxSize'].forEach(function (prop) {
- var length = parseInt(seriesOptions[prop], 10),
- isPercent = /%$/.test(length);
- extremes[prop] = isPercent ?
- smallestSize * length / 100 :
- length;
- });
- chart.minRadius = minSize = extremes.minSize;
- chart.maxRadius = maxSize = extremes.maxSize;
- (allDataPoints || []).forEach(function (point, i) {
- value = point[2];
- radius = series.getRadius(
- minSize,
- maxSize,
- minSize,
- maxSize,
- value
- );
- if (value === 0) {
- radius = null;
- }
- allDataPoints[i][2] = radius;
- radii.push(radius);
- });
- this.radii = radii;
- },
- alignDataLabel: H.Series.prototype.alignDataLabel
- }
- );
- // When one series is modified, the others need to be recomputed
- H.addEvent(H.seriesTypes.packedbubble, 'updatedData', function () {
- var self = this;
- this.chart.series.forEach(function (s) {
- if (s.type === self.type) {
- s.isDirty = true;
- }
- });
- });
- // Remove accumulated data points to redistribute all of them again
- // (i.e after hiding series by legend)
- H.addEvent(H.Chart, 'beforeRedraw', function () {
- if (this.allDataPoints) {
- delete this.allDataPoints;
- }
- });
- /**
- * A `packedbubble` series. If the [type](#series.packedbubble.type) option is
- * not specified, it is inherited from [chart.type](#chart.type).
- *
- * @extends series,plotOptions.packedbubble
- * @excluding dataParser, dataURL, stack
- * @product highcharts highstock
- * @apioption series.packedbubble
- */
- /**
- * An array of data points for the series. For the `packedbubble` series type,
- * points can be given in the following ways:
- *
- * 1. An array of `value` values.
- * ```js
- * data: [5, 1, 20]
- * ```
- *
- * 2. An array of objects with named values. The objects are point configuration
- * objects as seen below. If the total number of data points exceeds the
- * series' [turboThreshold](#series.packedbubble.turboThreshold), this option
- * is not available.
- * ```js
- * data: [{
- * value: 1,
- * name: "Point2",
- * color: "#00FF00"
- * }, {
- * value: 5,
- * name: "Point1",
- * color: "#FF00FF"
- * }]
- * ```
- *
- * @sample {highcharts} highcharts/series/data-array-of-objects/
- * Config objects
- *
- * @type {Array<number|*>}
- * @extends series.line.data
- * @excluding marker,x,y
- * @product highcharts
- * @apioption series.packedbubble.data
- */
- /**
- * The value of a bubble. The bubble's size proportional to its `value`.
- *
- * @type {number}
- * @product highcharts
- * @apioption series.packedbubble.data.weight
- */
- /**
- * @excluding enabled, enabledThreshold, height, radius, width
- * @apioption series.packedbubble.marker
- */
|