highcharts
Version:
JavaScript charting framework
387 lines (386 loc) • 13.4 kB
JavaScript
/* *
*
* (c) 2010-2024 Highsoft AS
*
* License: www.highcharts.com/license
*
* !!!!!!! SOURCE GETS TRANSPILED BY TYPESCRIPT. EDIT TS FILE ONLY. !!!!!!!
*
* */
;
import Geometry from './GeometryUtilities.js';
const { getAngleBetweenPoints, getCenterOfPoints, getDistanceBetweenPoints } = Geometry;
/* *
*
* Namespace
*
* */
var CircleUtilities;
(function (CircleUtilities) {
/* *
*
* Functions
*
* */
/**
* @private
*
* @param {number} x
* Number to round
*
* @param {number} decimals
* Number of decimals to round to
*
* @return {number}
* Rounded number
*/
function round(x, decimals) {
const a = Math.pow(10, decimals);
return Math.round(x * a) / a;
}
CircleUtilities.round = round;
/**
* Calculates the area of a circle based on its radius.
*
* @private
*
* @param {number} r
* The radius of the circle.
*
* @return {number}
* Returns the area of the circle.
*/
function getAreaOfCircle(r) {
if (r <= 0) {
throw new Error('radius of circle must be a positive number.');
}
return Math.PI * r * r;
}
CircleUtilities.getAreaOfCircle = getAreaOfCircle;
/**
* Calculates the area of a circular segment based on the radius of the
* circle and the height of the segment.
*
* @see http://mathworld.wolfram.com/CircularSegment.html
*
* @private
*
* @param {number} r
* The radius of the circle.
*
* @param {number} h
* The height of the circular segment.
*
* @return {number}
* Returns the area of the circular segment.
*/
function getCircularSegmentArea(r, h) {
return (r * r * Math.acos(1 - h / r) -
(r - h) * Math.sqrt(h * (2 * r - h)));
}
CircleUtilities.getCircularSegmentArea = getCircularSegmentArea;
/**
* Calculates the area of overlap between two circles based on their
* radiuses and the distance between them.
*
* @see http://mathworld.wolfram.com/Circle-CircleIntersection.html
*
* @private
*
* @param {number} r1
* Radius of the first circle.
*
* @param {number} r2
* Radius of the second circle.
*
* @param {number} d
* The distance between the two circles.
*
* @return {number}
* Returns the area of overlap between the two circles.
*/
function getOverlapBetweenCircles(r1, r2, d) {
let overlap = 0;
// If the distance is larger than the sum of the radiuses then the
// circles does not overlap.
if (d < r1 + r2) {
if (d <= Math.abs(r2 - r1)) {
// If the circles are completely overlapping, then the overlap
// equals the area of the smallest circle.
overlap = getAreaOfCircle(r1 < r2 ? r1 : r2);
}
else {
// Height of first triangle segment.
const d1 = (r1 * r1 - r2 * r2 + d * d) / (2 * d),
// Height of second triangle segment.
d2 = d - d1;
overlap = (getCircularSegmentArea(r1, r1 - d1) +
getCircularSegmentArea(r2, r2 - d2));
}
// Round the result to two decimals.
overlap = round(overlap, 14);
}
return overlap;
}
CircleUtilities.getOverlapBetweenCircles = getOverlapBetweenCircles;
/**
* Calculates the intersection points of two circles.
*
* NOTE: does not handle floating errors well.
*
* @private
*
* @param {Highcharts.CircleObject} c1
* The first circle.
*
* @param {Highcharts.CircleObject} c2
* The second circle.
*
* @return {Array<Highcharts.PositionObject>}
* Returns the resulting intersection points.
*/
function getCircleCircleIntersection(c1, c2) {
const d = getDistanceBetweenPoints(c1, c2), r1 = c1.r, r2 = c2.r;
let points = [];
if (d < r1 + r2 && d > Math.abs(r1 - r2)) {
// If the circles are overlapping, but not completely overlapping,
// then it exists intersecting points.
const r1Square = r1 * r1, r2Square = r2 * r2,
// `d^2 - r^2 + R^2 / 2d`
x = (r1Square - r2Square + d * d) / (2 * d),
// `y^2 = R^2 - x^2`
y = Math.sqrt(r1Square - x * x), x1 = c1.x, x2 = c2.x, y1 = c1.y, y2 = c2.y, x0 = x1 + x * (x2 - x1) / d, y0 = y1 + x * (y2 - y1) / d, rx = -(y2 - y1) * (y / d), ry = -(x2 - x1) * (y / d);
points = [
{ x: round(x0 + rx, 14), y: round(y0 - ry, 14) },
{ x: round(x0 - rx, 14), y: round(y0 + ry, 14) }
];
}
return points;
}
CircleUtilities.getCircleCircleIntersection = getCircleCircleIntersection;
/**
* Calculates all the intersection points for between a list of circles.
*
* @private
*
* @param {Array<Highcharts.CircleObject>} circles
* The circles to calculate the points from.
*
* @return {Array<Highcharts.GeometryObject>}
* Returns a list of intersection points.
*/
function getCirclesIntersectionPoints(circles) {
return circles.reduce((points, c1, i, arr) => {
const additional = arr
.slice(i + 1)
.reduce((points, c2, j) => {
const indexes = [i, j + i + 1];
return points.concat(getCircleCircleIntersection(c1, c2).map((p) => {
p.indexes = indexes;
return p;
}));
}, []);
return points.concat(additional);
}, []);
}
CircleUtilities.getCirclesIntersectionPoints = getCirclesIntersectionPoints;
/**
* Tests whether the first circle is completely overlapping the second
* circle.
*
* @private
*
* @param {Highcharts.CircleObject} circle1
* The first circle.
*
* @param {Highcharts.CircleObject} circle2
* The second circle.
*
* @return {boolean}
* Returns true if circle1 is completely overlapping circle2, false if not.
*/
function isCircle1CompletelyOverlappingCircle2(circle1, circle2) {
return getDistanceBetweenPoints(circle1, circle2) + circle2.r < circle1.r + 1e-10;
}
CircleUtilities.isCircle1CompletelyOverlappingCircle2 = isCircle1CompletelyOverlappingCircle2;
/**
* Tests whether a point lies within a given circle.
* @private
* @param {Highcharts.PositionObject} point
* The point to test for.
*
* @param {Highcharts.CircleObject} circle
* The circle to test if the point is within.
*
* @return {boolean}
* Returns true if the point is inside, false if outside.
*/
function isPointInsideCircle(point, circle) {
return getDistanceBetweenPoints(point, circle) <= circle.r + 1e-10;
}
CircleUtilities.isPointInsideCircle = isPointInsideCircle;
/**
* Tests whether a point lies within a set of circles.
*
* @private
*
* @param {Highcharts.PositionObject} point
* The point to test.
*
* @param {Array<Highcharts.CircleObject>} circles
* The list of circles to test against.
*
* @return {boolean}
* Returns true if the point is inside all the circles, false if not.
*/
function isPointInsideAllCircles(point, circles) {
return !circles.some(function (circle) {
return !isPointInsideCircle(point, circle);
});
}
CircleUtilities.isPointInsideAllCircles = isPointInsideAllCircles;
/**
* Tests whether a point lies outside a set of circles.
*
* TODO: add unit tests.
*
* @private
*
* @param {Highcharts.PositionObject} point
* The point to test.
*
* @param {Array<Highcharts.CircleObject>} circles
* The list of circles to test against.
*
* @return {boolean}
* Returns true if the point is outside all the circles, false if not.
*/
function isPointOutsideAllCircles(point, circles) {
return !circles.some(function (circle) {
return isPointInsideCircle(point, circle);
});
}
CircleUtilities.isPointOutsideAllCircles = isPointOutsideAllCircles;
/**
* Calculates the points for the polygon of the intersection area between
* a set of circles.
*
* @private
*
* @param {Array<Highcharts.CircleObject>} circles
* List of circles to calculate polygon of.
*
* @return {Array<Highcharts.GeometryObject>}
* Return list of points in the intersection polygon.
*/
function getCirclesIntersectionPolygon(circles) {
return getCirclesIntersectionPoints(circles)
.filter(function (p) {
return isPointInsideAllCircles(p, circles);
});
}
CircleUtilities.getCirclesIntersectionPolygon = getCirclesIntersectionPolygon;
/**
* Calculate the path for the area of overlap between a set of circles.
*
* @todo handle cases with only 1 or 0 arcs.
*
* @private
*
* @param {Array<Highcharts.CircleObject>} circles
* List of circles to calculate area of.
*
* @return {Highcharts.GeometryIntersectionObject|undefined}
* Returns the path for the area of overlap. Returns an empty string if
* there are no intersection between all the circles.
*/
function getAreaOfIntersectionBetweenCircles(circles) {
let intersectionPoints = getCirclesIntersectionPolygon(circles), result;
if (intersectionPoints.length > 1) {
// Calculate the center of the intersection points.
const center = getCenterOfPoints(intersectionPoints);
intersectionPoints = intersectionPoints
// Calculate the angle between the center and the points.
.map(function (p) {
p.angle = getAngleBetweenPoints(center, p);
return p;
})
// Sort the points by the angle to the center.
.sort(function (a, b) {
return b.angle - a.angle;
});
const startPoint = intersectionPoints[intersectionPoints.length - 1];
const arcs = intersectionPoints
.reduce(function (data, p1) {
const { startPoint } = data, midPoint = getCenterOfPoints([startPoint, p1]);
// Calculate the arc from the intersection points and their
// circles.
const arc = p1.indexes
// Filter out circles that are not included in both
// intersection points.
.filter(function (index) {
return startPoint.indexes.indexOf(index) > -1;
})
// Iterate the circles of the intersection points and
// calculate arcs.
.reduce(function (arc, index) {
const circle = circles[index], angle1 = getAngleBetweenPoints(circle, p1), angle2 = getAngleBetweenPoints(circle, startPoint), angleDiff = angle2 - angle1 +
(angle2 < angle1 ? 2 * Math.PI : 0), angle = angle2 - angleDiff / 2;
let width = getDistanceBetweenPoints(midPoint, {
x: circle.x + circle.r * Math.sin(angle),
y: circle.y + circle.r * Math.cos(angle)
});
const { r } = circle;
// Width can sometimes become to large due to
// floating point errors
if (width > r * 2) {
width = r * 2;
}
// Get the arc with the smallest width.
if (!arc || arc.width > width) {
arc = {
r,
largeArc: width > r ? 1 : 0,
width,
x: p1.x,
y: p1.y
};
}
// Return the chosen arc.
return arc;
}, null);
// If we find an arc then add it to the list and update p2.
if (arc) {
const { r } = arc;
data.arcs.push(['A', r, r, 0, arc.largeArc, 1, arc.x, arc.y]);
data.startPoint = p1;
}
return data;
}, {
startPoint: startPoint,
arcs: []
}).arcs;
if (arcs.length === 0) {
// Empty
}
else if (arcs.length === 1) {
// Empty
}
else {
arcs.unshift(['M', startPoint.x, startPoint.y]);
result = {
center,
d: arcs
};
}
}
return result;
}
CircleUtilities.getAreaOfIntersectionBetweenCircles = getAreaOfIntersectionBetweenCircles;
})(CircleUtilities || (CircleUtilities = {}));
/* *
*
* Default Export
*
* */
export default CircleUtilities;