initial commit
This commit is contained in:
@@ -0,0 +1,567 @@
|
||||
/* *
|
||||
* (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
|
||||
* @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
|
||||
*/
|
||||
Reference in New Issue
Block a user