UNPKG

@amcharts/amcharts5

Version:
441 lines 15.4 kB
import { isNumber } from "./Type"; /** * ============================================================================ * CONSTANTS * ============================================================================ * @hidden */ export const PI = Math.PI; export const HALFPI = PI / 2; export const RADIANS = PI / 180; export const DEGREES = 180 / PI; /** * Rounds the numeric value to whole number or specific precision of set. * * @param value Value * @param precision Precision (number of decimal points) * @param floor In case value ends with 0.5 and precision is 0, we might need to floor the value instead of ceiling it. * @return Rounded value */ export function round(value, precision, floor) { if (!isNumber(precision) || precision <= 0) { let rounded = Math.round(value); if (floor) { if (rounded - value == 0.5) { rounded--; } } return rounded; } else { let d = Math.pow(10, precision); return Math.round(value * d) / d; } } /** * Ceils the numeric value to whole number or specific precision of set. * * @param value Value * @param precision Precision (number of decimal points) * @return Rounded value */ export function ceil(value, precision) { if (!isNumber(precision) || precision <= 0) { return Math.ceil(value); } else { let d = Math.pow(10, precision); return Math.ceil(value * d) / d; } } /** * Returns the first control point for a cubic bezier spline segment * interpolating through three consecutive points with the given tension. * * @ignore * @param p0 Previous point * @param p1 Current point * @param p2 Next point * @param tensionX Horizontal tension (0–1) * @param tensionY Vertical tension (0–1) * @return First control point */ export function getCubicControlPointA(p0, p1, p2, tensionX, tensionY) { return { x: ((-p0.x + p1.x / tensionX + p2.x) * tensionX), y: ((-p0.y + p1.y / tensionY + p2.y) * tensionY) }; } /** * Returns the second control point for a cubic bezier spline segment * interpolating through three consecutive points with the given tension. * * @ignore * @param p1 Current point * @param p2 Next point * @param p3 Point after next * @param tensionX Horizontal tension (0–1) * @param tensionY Vertical tension (0–1) * @return Second control point */ export function getCubicControlPointB(p1, p2, p3, tensionX, tensionY) { return { x: ((p1.x + p2.x / tensionX - p3.x) * tensionX), y: ((p1.y + p2.y / tensionY - p3.y) * tensionY) }; } /** * Clamps a value to the given [min, max] range. * * @param value Value to clamp * @param min Minimum * @param max Maximum * @return Clamped value */ export function fitToRange(value, min, max) { return Math.min(Math.max(value, min), max); } /** * Returns sine of an angle specified in degrees. * * @param value Value * @return Sine */ export function sin(angle) { return Math.sin(RADIANS * angle); } /** * Returns tan of an angle specified in degrees. * * @param value Value * @return Sine */ export function tan(angle) { return Math.tan(RADIANS * angle); } /** * Returns cosine of an angle specified in degrees. * * @param value Value * @return Cosine */ export function cos(angle) { return Math.cos(RADIANS * angle); } /** * Normalizes an angle to the 0–360 range. * * @param value Angle in degrees * @return Normalized angle (0–360) */ export function normalizeAngle(value) { value = value % 360; if (value < 0) { value += 360; } return value; } /** * Returns the bounding box of a circular arc. * * @param cx Center X * @param cy Center Y * @param startAngle Start angle in degrees * @param endAngle End angle in degrees * @param radius Arc radius * @return Bounding box */ export function getArcBounds(cx, cy, startAngle, endAngle, radius) { // Guard against non-finite angles (e.g. a radar chart whose start/end angle // was set to NaN or Infinity) and against a full-circle-or-more span. Without // this, the 90°-step loop below runs forever (Infinity) or returns NaN bounds. // In both cases fall back to the full-radius box so callers stay finite. if (!Number.isFinite(startAngle) || !Number.isFinite(endAngle) || Math.abs(endAngle - startAngle) >= 360) { const r = Number.isFinite(radius) ? Math.abs(radius) : 0; return ({ left: cx - r, top: cy - r, right: cx + r, bottom: cy + r }); } let minX = Number.MAX_VALUE; let minY = Number.MAX_VALUE; let maxX = -Number.MAX_VALUE; let maxY = -Number.MAX_VALUE; let bpoints = []; bpoints.push(getArcPoint(radius, startAngle)); bpoints.push(getArcPoint(radius, endAngle)); let fromAngle = Math.min(Math.floor(startAngle / 90) * 90, Math.floor(endAngle / 90) * 90); let toAngle = Math.max(Math.ceil(startAngle / 90) * 90, Math.ceil(endAngle / 90) * 90); for (let angle = fromAngle; angle <= toAngle; angle += 90) { if (angle >= startAngle && angle <= endAngle) { bpoints.push(getArcPoint(radius, angle)); } } for (let i = 0; i < bpoints.length; i++) { let pt = bpoints[i]; if (pt.x < minX) { minX = pt.x; } if (pt.y < minY) { minY = pt.y; } if (pt.x > maxX) { maxX = pt.x; } if (pt.y > maxY) { maxY = pt.y; } } return ({ left: cx + minX, top: cy + minY, right: cx + maxX, bottom: cy + maxY }); } /** * Returns a point on a circle at the given angle. * * @param radius Circle radius * @param arc Angle in degrees * @return Point on the arc */ export function getArcPoint(radius, arc) { return ({ x: radius * cos(arc), y: radius * sin(arc) }); } /** * Merges an array of bounds into a single bounding box that encompasses all of them. * * @param bounds Array of bounds to merge * @return Combined bounding box */ export function mergeBounds(bounds) { const len = bounds.length; if (len > 0) { let bound = bounds[0]; let left = bound.left; let top = bound.top; let right = bound.right; let bottom = bound.bottom; if (len > 1) { for (let i = 1; i < len; i++) { bound = bounds[i]; left = Math.min(bound.left, left); right = Math.max(bound.right, right); top = Math.min(bound.top, top); bottom = Math.max(bound.bottom, bottom); } } return { left, right, top, bottom }; } return { left: 0, right: 0, top: 0, bottom: 0 }; } /** * Fits an angle into the given start/end range, snapping to the * nearest boundary when the angle falls outside. * * @param value Angle in degrees * @param startAngle Range start in degrees * @param endAngle Range end in degrees * @return Angle clamped to the range */ export function fitAngleToRange(value, startAngle, endAngle) { if (startAngle > endAngle) { let temp = startAngle; startAngle = endAngle; endAngle = temp; } value = normalizeAngle(value); let count = (startAngle - normalizeAngle(startAngle)) / 360; if (value < startAngle) { value += 360 * (count + 1); } let maxEnd = startAngle + (endAngle - startAngle) / 2 + 180; let maxStart = startAngle + (endAngle - startAngle) / 2 - 180; if (value > endAngle) { if (value - 360 > startAngle) { value -= 360; } else { if (value < maxEnd) { value = endAngle; } else { value = startAngle; } } } if (value < startAngle) { if (value > maxStart) { value = startAngle; } else { value = endAngle; } } return value; } /** * Returns `true` if a point is inside the given bounds (inclusive). * * @param point Point to test * @param bounds Bounding box * @return Whether the point is inside */ export function inBounds(point, bounds) { if (point.x >= bounds.left && point.y >= bounds.top && point.x <= bounds.right && point.y <= bounds.bottom) { return true; } return false; } /** * Returns the angle in degrees from `point1` to `point2`. * If `point2` is omitted, uses double of `point1` coordinates. * * @param point1 Origin point * @param point2 Target point (optional) * @return Angle in degrees (0–360) */ export function getAngle(point1, point2) { if (!point2) { point2 = { x: point1.x * 2, y: point1.y * 2 }; } let diffX = point2.x - point1.x; let diffY = point2.y - point1.y; let angle = Math.atan2(diffY, diffX) * DEGREES; if (angle < 0) { angle += 360; } return normalizeAngle(angle); } /** * Returns a point on a quadratic bezier curve at the given position (0–1). * * @param pointA Start point * @param pointB End point * @param controlPoint Control point * @param position Relative position (0 = start, 1 = end) * @return Point on the curve */ export function getPointOnQuadraticCurve(pointA, pointB, controlPoint, position) { let x = (1 - position) * (1 - position) * pointA.x + 2 * (1 - position) * position * controlPoint.x + position * position * pointB.x; let y = (1 - position) * (1 - position) * pointA.y + 2 * (1 - position) * position * controlPoint.y + position * position * pointB.y; return { x: x, y: y }; } /** * Returns a point on a cubic bezier curve at the given position (0–1). * * @param pointA Start point * @param pointB End point * @param controlPointA First control point (near start) * @param controlPointB Second control point (near end) * @param position Relative position (0 = start, 1 = end) * @return Point on the curve */ export function getPointOnCubicCurve(pointA, pointB, controlPointA, controlPointB, position) { let s = 1 - position; let x = s * s * s * pointA.x + 3 * s * s * position * controlPointA.x + 3 * s * position * position * controlPointB.x + position * position * position * pointB.x; let y = s * s * s * pointA.y + 3 * s * s * position * controlPointA.y + 3 * s * position * position * controlPointB.y + position * position * position * pointB.y; return { x: x, y: y }; } /** * Returns a point at a relative position along a straight line between two points. * * @param pointA Start point * @param pointB End point * @param position Relative position (0 = start, 1 = end) * @return Point on the line */ export function getPointOnLine(pointA, pointB, position) { return { x: pointA.x + (pointB.x - pointA.x) * position, y: pointA.y + (pointB.y - pointA.y) * position }; } /** * Given a normalized location (0–1) along a multi-segment path and an array * of cumulative segment lengths, returns which segment the location falls in * and the local parameter t within that segment. * * @param location Relative position along the full path (0–1) * @param cumulativeLengths Cumulative length at the end of each segment * @return Segment index and local t (0–1) */ export function resolveLocationOnPath(location, cumulativeLengths) { const n = cumulativeLengths.length; if (n > 0) { const totalLength = cumulativeLengths[n - 1]; const targetLength = location * totalLength; let index = 0; for (let i = 0; i < n; i++) { if (cumulativeLengths[i] >= targetLength) { index = i; break; } } const segStart = index > 0 ? cumulativeLengths[index - 1] : 0; const segLength = cumulativeLengths[index] - segStart; const t = segLength > 0 ? (targetLength - segStart) / segLength : 0; return { index, t }; } return { index: 0, t: 0 }; } /** * Returns the closest value from the array of values to the reference value. * * @param values Array of values * @param value Reference value * @return Closes value from the array */ export function closest(values, referenceValue) { return values.reduce(function (prev, curr) { return (Math.abs(curr - referenceValue) < Math.abs(prev - referenceValue) ? curr : prev); }); } /** * Returns true if bounds overlap * @param bounds1 IBounds * @param bounds2 IBounds * @returns boolean */ export function boundsOverlap(bounds1, bounds2) { const horizontalOverlap = bounds1.left < bounds2.right && bounds1.right > bounds2.left; const verticalOverlap = bounds1.top < bounds2.bottom && bounds1.bottom > bounds2.top; return horizontalOverlap && verticalOverlap; } /** * Generates points along a spiral path. * * @param cx Center X * @param cy Center Y * @param radius Outer radius * @param radiusY Vertical radius (for elliptical spirals) * @param innerRadius Inner radius where the spiral starts * @param step Base step size between points * @param radiusStep Radius increase per full revolution * @param startAngle Start angle in degrees * @param endAngle End angle in degrees * @return Array of points along the spiral */ export function spiralPoints(cx, cy, radius, radiusY, innerRadius, step, radiusStep, startAngle, endAngle) { let r = innerRadius + 0.01; startAngle = normalizeAngle(startAngle); endAngle = normalizeAngle(endAngle); let angle = startAngle * RADIANS; if (endAngle < startAngle) { endAngle += 360; } let points = []; while (r < radius + radiusStep) { let stepSize = step; if (stepSize / 2 > r) { stepSize = 2 * r; } let c = Math.max(0.01, Math.min(1, r / 200)); stepSize = stepSize * c; let degrees = angle * DEGREES; let point = { x: cx + r * Math.cos(angle), y: cy + r * radiusY / radius * Math.sin(angle) }; points.push(point); r = innerRadius + 0.01 + (degrees - startAngle) / 360 * radiusStep; angle += 2 * Math.asin(stepSize / 2 / r); if (angle * DEGREES > endAngle + 360 * Math.ceil((radius - innerRadius) / radiusStep)) { break; } } points.shift(); return points; } /** * Returns `true` if two circles overlap or touch. * * @param circle1 First circle (x, y, radius) * @param circle2 Second circle (x, y, radius) * @return Whether the circles overlap */ export function circlesOverlap(circle1, circle2) { return Math.hypot(circle1.x - circle2.x, circle1.y - circle2.y) <= circle1.radius + circle2.radius; } //# sourceMappingURL=Math.js.map