highcharts
Version:
JavaScript charting framework
716 lines (715 loc) • 25.2 kB
JavaScript
/* *
*
* (c) 2010-2025 Torstein Honsi
*
* License: www.highcharts.com/license
*
* !!!!!!! SOURCE GETS TRANSPILED BY TYPESCRIPT. EDIT TS FILE ONLY. !!!!!!!
*
* */
'use strict';
import BubbleLegendComposition from './BubbleLegendComposition.js';
import BubblePoint from './BubblePoint.js';
import H from '../../Core/Globals.js';
const { composed, noop } = H;
import SeriesRegistry from '../../Core/Series/SeriesRegistry.js';
const { series: Series, seriesTypes: { column: { prototype: columnProto }, scatter: ScatterSeries } } = SeriesRegistry;
import U from '../../Core/Utilities.js';
const { addEvent, arrayMax, arrayMin, clamp, extend, isNumber, merge, pick, pushUnique } = U;
/* *
*
* Functions
*
* */
/**
* Add logic to pad each axis with the amount of pixels necessary to avoid the
* bubbles to overflow.
*/
function onAxisFoundExtremes() {
const axisLength = this.len, { coll, isXAxis, min } = this, range = (this.max || 0) - (min || 0);
let pxMin = 0, pxMax = axisLength, transA = axisLength / range, hasActiveSeries;
if (coll !== 'xAxis' && coll !== 'yAxis') {
return;
}
// Handle padding on the second pass, or on redraw
this.series.forEach((series) => {
if (series.bubblePadding && series.reserveSpace()) {
// Correction for #1673
this.allowZoomOutside = true;
hasActiveSeries = true;
const data = series.getColumn(isXAxis ? 'x' : 'y');
if (isXAxis) {
(series.onPoint || series).getRadii(0, 0, series);
if (series.onPoint) {
series.radii = series.onPoint.radii;
}
}
if (range > 0) {
let i = data.length;
while (i--) {
if (isNumber(data[i]) &&
this.dataMin <= data[i] &&
data[i] <= this.max) {
const radius = series.radii && series.radii[i] || 0;
pxMin = Math.min(((data[i] - min) * transA) - radius, pxMin);
pxMax = Math.max(((data[i] - min) * transA) + radius, pxMax);
}
}
}
}
});
// Apply the padding to the min and max properties
if (hasActiveSeries && range > 0 && !this.logarithmic) {
pxMax -= axisLength;
transA *= (axisLength +
Math.max(0, pxMin) - // #8901
Math.min(pxMax, axisLength)) / axisLength;
[
['min', 'userMin', pxMin],
['max', 'userMax', pxMax]
].forEach((keys) => {
if (typeof pick(this.options[keys[0]], this[keys[1]]) === 'undefined') {
this[keys[0]] += keys[2] / transA;
}
});
}
}
/**
* If a user has defined categories, it is necessary to retroactively hide any
* ticks added by the 'onAxisFoundExtremes' function above (#21672).
*
* Otherwise they can show up on the axis, alongside user-defined categories.
*/
function onAxisAfterRender() {
const { ticks, tickPositions, dataMin = 0, dataMax = 0, categories } = this, type = this.options.type;
if ((categories?.length || type === 'category') &&
this.series.find((s) => s.bubblePadding)) {
let tickCount = tickPositions.length;
while (tickCount--) {
const tick = ticks[tickPositions[tickCount]], pos = tick.pos || 0;
if (pos > dataMax || pos < dataMin) {
tick.label?.hide();
}
}
}
}
/* *
*
* Class
*
* */
class BubbleSeries extends ScatterSeries {
/* *
*
* Static Functions
*
* */
static compose(AxisClass, ChartClass, LegendClass) {
BubbleLegendComposition.compose(ChartClass, LegendClass);
if (pushUnique(composed, 'Series.Bubble')) {
addEvent(AxisClass, 'foundExtremes', onAxisFoundExtremes);
addEvent(AxisClass, 'afterRender', onAxisAfterRender);
}
}
/* *
*
* Functions
*
* */
/**
* Perform animation on the bubbles
* @private
*/
animate(init) {
if (!init &&
this.points.length < this.options.animationLimit // #8099
) {
this.points.forEach(function (point) {
const { graphic, plotX = 0, plotY = 0 } = point;
if (graphic && graphic.width) { // URL symbols don't have width
// Start values
if (!this.hasRendered) {
graphic.attr({
x: plotX,
y: plotY,
width: 1,
height: 1
});
}
graphic.animate(this.markerAttribs(point), this.options.animation);
}
}, this);
}
}
/**
* Get the radius for each point based on the minSize, maxSize and each
* point's Z value. This must be done prior to Series.translate because
* the axis needs to add padding in accordance with the point sizes.
* @private
*/
getRadii() {
const zData = this.getColumn('z'), yData = this.getColumn('y'), radii = [];
let len, i, value, zExtremes = this.chart.bubbleZExtremes;
const { minPxSize, maxPxSize } = this.getPxExtremes();
// Get the collective Z extremes of all bubblish series. The chart-level
// `bubbleZExtremes` are only computed once, and reset on `updatedData`
// in any member series.
if (!zExtremes) {
let zMin = Number.MAX_VALUE;
let zMax = -Number.MAX_VALUE;
let valid;
this.chart.series.forEach((otherSeries) => {
if (otherSeries.bubblePadding && otherSeries.reserveSpace()) {
const zExtremes = (otherSeries.onPoint || otherSeries).getZExtremes();
if (zExtremes) {
// Changed '||' to 'pick' because min or max can be 0.
// #17280
zMin = Math.min(pick(zMin, zExtremes.zMin), zExtremes.zMin);
zMax = Math.max(pick(zMax, zExtremes.zMax), zExtremes.zMax);
valid = true;
}
}
});
if (valid) {
zExtremes = { zMin, zMax };
this.chart.bubbleZExtremes = zExtremes;
}
else {
zExtremes = { zMin: 0, zMax: 0 };
}
}
// Set the shape type and arguments to be picked up in drawPoints
for (i = 0, len = zData.length; i < len; i++) {
value = zData[i];
// Separate method to get individual radius for bubbleLegend
radii.push(this.getRadius(zExtremes.zMin, zExtremes.zMax, minPxSize, maxPxSize, value, yData && yData[i]));
}
this.radii = radii;
}
/**
* Get the individual radius for one point.
* @private
*/
getRadius(zMin, zMax, minSize, maxSize, value, yValue) {
const options = this.options, sizeByArea = options.sizeBy !== 'width', zThreshold = options.zThreshold;
let zRange = zMax - zMin, pos = 0.5;
// #8608 - bubble should be visible when z is undefined
if (yValue === null || value === null) {
return null;
}
if (isNumber(value)) {
// When sizing by threshold, the absolute value of z determines
// the size of the bubble.
if (options.sizeByAbsoluteValue) {
value = Math.abs(value - zThreshold);
zMax = zRange = Math.max(zMax - zThreshold, Math.abs(zMin - zThreshold));
zMin = 0;
}
// Issue #4419 - if value is less than zMin, push a radius that's
// always smaller than the minimum size
if (value < zMin) {
return minSize / 2 - 1;
}
// Relative size, a number between 0 and 1
if (zRange > 0) {
pos = (value - zMin) / zRange;
}
}
if (sizeByArea && pos >= 0) {
pos = Math.sqrt(pos);
}
return Math.ceil(minSize + pos * (maxSize - minSize)) / 2;
}
/**
* Define hasData function for non-cartesian series.
* Returns true if the series has points at all.
* @private
*/
hasData() {
return !!this.dataTable.rowCount;
}
/**
* @private
*/
markerAttribs(point, state) {
const attr = super.markerAttribs(point, state), { height = 0, width = 0 } = attr;
// Bubble needs a specific `markerAttribs` override because the markers
// are rendered into the potentially inverted `series.group`. Unlike
// regular markers, which are rendered into the `markerGroup` (#21125).
return this.chart.inverted ? extend(attr, {
x: (point.plotX || 0) - width / 2,
y: (point.plotY || 0) - height / 2
}) : attr;
}
/**
* @private
*/
pointAttribs(point, state) {
const markerOptions = this.options.marker, fillOpacity = markerOptions?.fillOpacity, attr = Series.prototype.pointAttribs.call(this, point, state);
attr['fill-opacity'] = fillOpacity ?? 1;
return attr;
}
/**
* Extend the base translate method to handle bubble size
* @private
*/
translate() {
// Run the parent method
super.translate.call(this);
this.getRadii();
this.translateBubble();
}
translateBubble() {
const { data, options, radii } = this, { minPxSize } = this.getPxExtremes();
// Set the shape type and arguments to be picked up in drawPoints
let i = data.length;
while (i--) {
const point = data[i], radius = radii ? radii[i] : 0; // #1737
// Negative points means negative z values (#9728)
if (this.zoneAxis === 'z') {
point.negative = (point.z || 0) < (options.zThreshold || 0);
}
if (isNumber(radius) && radius >= minPxSize / 2) {
// Shape arguments
point.marker = extend(point.marker, {
radius,
width: 2 * radius,
height: 2 * radius
});
// Alignment box for the data label
point.dlBox = {
x: point.plotX - radius,
y: point.plotY - radius,
width: 2 * radius,
height: 2 * radius
};
}
else { // Below zThreshold
// #1691
point.shapeArgs = point.plotY = point.dlBox = void 0;
point.isInside = false; // #17281
}
}
}
getPxExtremes() {
const smallestSize = Math.min(this.chart.plotWidth, this.chart.plotHeight);
const getPxSize = (length) => {
let isPercent;
if (typeof length === 'string') {
isPercent = /%$/.test(length);
length = parseInt(length, 10);
}
return isPercent ? smallestSize * length / 100 : length;
};
const minPxSize = getPxSize(pick(this.options.minSize, 8));
// Prioritize min size if conflict to make sure bubbles are
// always visible. #5873
const maxPxSize = Math.max(getPxSize(pick(this.options.maxSize, '20%')), minPxSize);
return { minPxSize, maxPxSize };
}
getZExtremes() {
const options = this.options, zData = this.getColumn('z').filter(isNumber);
if (zData.length) {
const zMin = pick(options.zMin, clamp(arrayMin(zData), options.displayNegative === false ?
(options.zThreshold || 0) :
-Number.MAX_VALUE, Number.MAX_VALUE));
const zMax = pick(options.zMax, arrayMax(zData));
if (isNumber(zMin) && isNumber(zMax)) {
return { zMin, zMax };
}
}
}
/**
* @private
* @function Highcharts.Series#searchKDTree
*/
searchKDTree(point, compareX, e, suppliedPointEvaluator = noop, suppliedBSideCheckEvaluator = noop) {
suppliedPointEvaluator = (p1, p2, comparisonProp) => {
const p1Dist = p1[comparisonProp] || 0;
const p2Dist = p2[comparisonProp] || 0;
let ret, flip = false;
if (p1Dist === p2Dist) {
ret = p1.index > p2.index ? p1 : p2;
}
else if (p1Dist < 0 && p2Dist < 0) {
ret = (p1Dist - (p1.marker?.radius || 0) >=
p2Dist - (p2.marker?.radius || 0)) ?
p1 :
p2;
flip = true;
}
else {
ret = p1Dist < p2Dist ? p1 : p2;
}
return [ret, flip];
};
suppliedBSideCheckEvaluator = (a, b, flip) => !flip && (a > b) || (a < b);
return super.searchKDTree(point, compareX, e, suppliedPointEvaluator, suppliedBSideCheckEvaluator);
}
}
/* *
*
* Static Properties
*
* */
/**
* A bubble series is a three dimensional series type where each point
* renders an X, Y and Z value. Each points is drawn as a bubble where the
* position along the X and Y axes mark the X and Y values, and the size of
* the bubble relates to the Z value.
*
* @sample {highcharts} highcharts/demo/bubble/
* Bubble chart
*
* @extends plotOptions.scatter
* @excluding cluster
* @product highcharts highstock
* @requires highcharts-more
* @optionparent plotOptions.bubble
*/
BubbleSeries.defaultOptions = merge(ScatterSeries.defaultOptions, {
dataLabels: {
formatter: function () {
const { numberFormatter } = this.series.chart;
const { z } = this.point;
return isNumber(z) ? numberFormatter(z, -1) : '';
},
inside: true,
verticalAlign: 'middle'
},
/**
* If there are more points in the series than the `animationLimit`, the
* animation won't run. Animation affects overall performance and
* doesn't work well with heavy data series.
*
* @since 6.1.0
*/
animationLimit: 250,
/**
* Whether to display negative sized bubbles. The threshold is given
* by the [zThreshold](#plotOptions.bubble.zThreshold) option, and negative
* bubbles can be visualized by setting
* [negativeColor](#plotOptions.bubble.negativeColor).
*
* @sample {highcharts} highcharts/plotoptions/bubble-negative/
* Negative bubbles
*
* @type {boolean}
* @default true
* @since 3.0
* @apioption plotOptions.bubble.displayNegative
*/
/**
* @extends plotOptions.series.marker
* @excluding enabled, enabledThreshold, height, radius, width
*/
marker: {
lineColor: null, // Inherit from series.color
lineWidth: 1,
/**
* The fill opacity of the bubble markers.
*/
fillOpacity: 0.5,
/**
* In bubble charts, the radius is overridden and determined based
* on the point's data value.
*
* @ignore-option
*/
radius: null,
states: {
hover: {
radiusPlus: 0
}
},
/**
* A predefined shape or symbol for the marker. Possible values are
* "circle", "square", "diamond", "triangle" and "triangle-down".
*
* Additionally, the URL to a graphic can be given on the form
* `url(graphic.png)`. Note that for the image to be applied to
* exported charts, its URL needs to be accessible by the export
* server.
*
* Custom callbacks for symbol path generation can also be added to
* `Highcharts.SVGRenderer.prototype.symbols`. The callback is then
* used by its method name, as shown in the demo.
*
* @sample {highcharts} highcharts/plotoptions/bubble-symbol/
* Bubble chart with various symbols
* @sample {highcharts} highcharts/plotoptions/series-marker-symbol/
* General chart with predefined, graphic and custom markers
*
* @type {Highcharts.SymbolKeyValue|string}
* @since 5.0.11
*/
symbol: 'circle'
},
/**
* Minimum bubble size. Bubbles will automatically size between the
* `minSize` and `maxSize` to reflect the `z` 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}
* @since 3.0
* @product highcharts highstock
*/
minSize: 8,
/**
* Maximum bubble size. Bubbles will automatically size between the
* `minSize` and `maxSize` to reflect the `z` 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}
* @since 3.0
* @product highcharts highstock
*/
maxSize: '20%',
/**
* When a point's Z value is below the
* [zThreshold](#plotOptions.bubble.zThreshold)
* setting, this color is used.
*
* @sample {highcharts} highcharts/plotoptions/bubble-negative/
* Negative bubbles
*
* @type {Highcharts.ColorString|Highcharts.GradientColorObject|Highcharts.PatternObject}
* @since 3.0
* @product highcharts
* @apioption plotOptions.bubble.negativeColor
*/
/**
* Whether the bubble's 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} highcharts/plotoptions/bubble-sizeby/
* Comparison of area and size
*
* @type {Highcharts.BubbleSizeByValue}
* @default area
* @since 3.0.7
* @apioption plotOptions.bubble.sizeBy
*/
/**
* 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`.
*
* @sample {highcharts} highcharts/plotoptions/bubble-sizebyabsolutevalue/
* Size by absolute value, various thresholds
*
* @type {boolean}
* @default false
* @since 4.1.9
* @product highcharts
* @apioption plotOptions.bubble.sizeByAbsoluteValue
*/
/**
* When this is true, the series will not cause the Y axis to cross
* the zero plane (or [threshold](#plotOptions.series.threshold) option)
* unless the data actually crosses the plane.
*
* For example, if `softThreshold` is `false`, a series of 0, 1, 2,
* 3 will make the Y axis show negative values according to the
* `minPadding` option. If `softThreshold` is `true`, the Y axis starts
* at 0.
*
* @since 4.1.9
* @product highcharts
*/
softThreshold: false,
states: {
hover: {
halo: {
size: 5
}
}
},
tooltip: {
pointFormat: '({point.x}, {point.y}), Size: {point.z}'
},
turboThreshold: 0,
/**
* The minimum for the Z value range. Defaults to the highest Z value
* in the data.
*
* @see [zMin](#plotOptions.bubble.zMin)
*
* @sample {highcharts} highcharts/plotoptions/bubble-zmin-zmax/
* Z has a possible range of 0-100
*
* @type {number}
* @since 4.0.3
* @product highcharts
* @apioption plotOptions.bubble.zMax
*/
/**
* @default z
* @apioption plotOptions.bubble.colorKey
*/
/**
* The minimum for the Z value range. Defaults to the lowest Z value
* in the data.
*
* @see [zMax](#plotOptions.bubble.zMax)
*
* @sample {highcharts} highcharts/plotoptions/bubble-zmin-zmax/
* Z has a possible range of 0-100
*
* @type {number}
* @since 4.0.3
* @product highcharts
* @apioption plotOptions.bubble.zMin
*/
/**
* When [displayNegative](#plotOptions.bubble.displayNegative) is `false`,
* bubbles with lower Z values are skipped. When `displayNegative`
* is `true` and a [negativeColor](#plotOptions.bubble.negativeColor)
* is given, points with lower Z is colored.
*
* @sample {highcharts} highcharts/plotoptions/bubble-negative/
* Negative bubbles
*
* @since 3.0
* @product highcharts
*/
zThreshold: 0,
zoneAxis: 'z'
});
extend(BubbleSeries.prototype, {
alignDataLabel: columnProto.alignDataLabel,
applyZones: noop,
bubblePadding: true,
isBubble: true,
keysAffectYAxis: ['y'],
pointArrayMap: ['y', 'z'],
pointClass: BubblePoint,
parallelArrays: ['x', 'y', 'z'],
trackerGroups: ['group', 'dataLabelsGroup'],
specialGroup: 'group', // To allow clipping (#6296)
zoneAxis: 'z'
});
// On updated data in any series, delete the chart-level Z extremes cache
addEvent(BubbleSeries, 'updatedData', (e) => {
delete e.target.chart.bubbleZExtremes;
});
// After removing series, delete the chart-level Z extremes cache, #17502.
addEvent(BubbleSeries, 'remove', (e) => {
delete e.target.chart.bubbleZExtremes;
});
SeriesRegistry.registerSeriesType('bubble', BubbleSeries);
/* *
*
* Default Export
*
* */
export default BubbleSeries;
/* *
*
* API Declarations
*
* */
/**
* @typedef {"area"|"width"} Highcharts.BubbleSizeByValue
*/
''; // Detach doclets above
/* *
*
* API Options
*
* */
/**
* A `bubble` series. If the [type](#series.bubble.type) option is
* not specified, it is inherited from [chart.type](#chart.type).
*
* @extends series,plotOptions.bubble
* @excluding dataParser, dataURL, legendSymbolColor, stack
* @product highcharts highstock
* @requires highcharts-more
* @apioption series.bubble
*/
/**
* An array of data points for the series. For the `bubble` series type,
* points can be given in the following ways:
*
* 1. An array of arrays with 3 or 2 values. In this case, the values correspond
* to `x,y,z`. If the first value is a string, it is applied as the name of
* the point, and the `x` value is inferred. The `x` value can also be
* omitted, in which case the inner arrays should be of length 2\. Then the
* `x` value is automatically calculated, either starting at 0 and
* incremented by 1, or from `pointStart` and `pointInterval` given in the
* series options.
* ```js
* data: [
* [0, 1, 2],
* [1, 5, 5],
* [2, 0, 2]
* ]
* ```
*
* 2. An array of objects with named values. The following snippet shows only a
* few settings, see the complete options set below. If the total number of
* data points exceeds the series'
* [turboThreshold](#series.bubble.turboThreshold), this option is not
* available.
* ```js
* data: [{
* x: 1,
* y: 1,
* z: 1,
* name: "Point2",
* color: "#00FF00"
* }, {
* x: 1,
* y: 5,
* z: 4,
* name: "Point1",
* color: "#FF00FF"
* }]
* ```
*
* @sample {highcharts} highcharts/series/data-array-of-arrays/
* Arrays of numeric x and y
* @sample {highcharts} highcharts/series/data-array-of-arrays-datetime/
* Arrays of datetime x and y
* @sample {highcharts} highcharts/series/data-array-of-name-value/
* Arrays of point.name and y
* @sample {highcharts} highcharts/series/data-array-of-objects/
* Config objects
*
* @type {Array<Array<(number|string),number>|Array<(number|string),number,number>|*>}
* @extends series.line.data
* @product highcharts
* @apioption series.bubble.data
*/
/**
* @extends series.line.data.marker
* @excluding enabledThreshold, height, radius, width
* @product highcharts
* @apioption series.bubble.data.marker
*/
/**
* The size value for each bubble. The bubbles' diameters are computed
* based on the `z`, and controlled by series options like `minSize`,
* `maxSize`, `sizeBy`, `zMin` and `zMax`.
*
* @type {number|null}
* @product highcharts
* @apioption series.bubble.data.z
*/
/**
* @excluding enabled, enabledThreshold, height, radius, width
* @apioption series.bubble.marker
*/
''; // Adds doclets above to transpiled file