UNPKG

apexcharts

Version:

A JavaScript Chart Library

713 lines (628 loc) 16.7 kB
// @ts-check /* ** Generic functions which are not dependent on ApexCharts */ import { Environment } from './Environment.js' import { BrowserAPIs } from '../ssr/BrowserAPIs.js' class Utils { /** * @param {*} item */ static isObject(item) { return item && typeof item === 'object' && !Array.isArray(item) } // Type checking that works across different window objects /** * @param {string} type * @param {string} val */ static is(type, val) { return Object.prototype.toString.call(val) === '[object ' + type + ']' } static isSafari() { return ( Environment.isBrowser() && /^((?!chrome|android).)*safari/i.test(navigator.userAgent) ) } // to extend defaults with user options // credit: http://stackoverflow.com/questions/27936772/deep-object-merging-in-es6-es7#answer-34749873 /** * @param {any} target * @param {any} source */ static extend(target, source) { const output = Object.assign({}, target) if (this.isObject(target) && this.isObject(source)) { Object.keys(source).forEach((key) => { if (this.isObject(source[key])) { if (!(key in target)) { Object.assign(output, { [key]: source[key], }) } else { output[key] = this.extend(target[key], source[key]) } } else { Object.assign(output, { [key]: source[key], }) } }) } return output } /** * @param {any[]} arrToExtend * @param {any} resultArr */ static extendArray(arrToExtend, resultArr) { /** @type {any[]} */ const extendedArr = [] /** * @param {any} item */ arrToExtend.map((/** @type {any} */ item) => { extendedArr.push(Utils.extend(resultArr, item)) }) arrToExtend = extendedArr return arrToExtend } // If month counter exceeds 12, it starts again from 1 /** * @param {number} month */ static monthMod(month) { return month % 12 } /** * clone object with optional shallow copy for performance * @param {*} source - Source object to clone * @param {WeakMap<any, any>} visited - Circular reference tracker * @param {boolean} shallow - If true, performs shallow copy (default: false) * @returns {*} Cloned object */ static clone(source, visited = new WeakMap(), shallow = false) { if (source === null || typeof source !== 'object') { return source } if (visited.has(source)) { return visited.get(source) } /** @type {any} */ let cloneResult if (Array.isArray(source)) { if (shallow) { cloneResult = source.slice() } else { cloneResult = [] visited.set(source, cloneResult) for (let i = 0; i < source.length; i++) { cloneResult[i] = this.clone(source[i], visited, false) } } } else if (source instanceof Date) { cloneResult = new Date(source.getTime()) } else { if (shallow) { cloneResult = Object.assign({}, source) } else { cloneResult = {} visited.set(source, cloneResult) for (const prop in source) { if (Object.prototype.hasOwnProperty.call(source, prop)) { cloneResult[prop] = this.clone( /** @type {Record<string,any>} */ (source)[prop], visited, false, ) } } } } return cloneResult } /** * Shallow clone for performance when deep clone isn't needed * @param {*} source - Source to clone * @returns {*} Shallow cloned object */ static shallowClone(source) { if (source === null || typeof source !== 'object') { return source } if (Array.isArray(source)) { return source.slice() } return Object.assign({}, source) } /** * Fast shallow equality check for objects * @param {Object} obj1 - First object * @param {Object} obj2 - Second object * @returns {boolean} True if shallowly equal */ static shallowEqual(obj1, obj2) { if (obj1 === obj2) return true if (!obj1 || !obj2) return false if (typeof obj1 !== 'object' || typeof obj2 !== 'object') { return obj1 === obj2 } const keys1 = Object.keys(obj1) const keys2 = Object.keys(obj2) if (keys1.length !== keys2.length) return false for (const key of keys1) { if ( /** @type {Record<string,any>} */ (obj1)[key] !== /** @type {Record<string,any>} */ (obj2)[key] ) return false } return true } /** * @param {number} x */ static log10(x) { return Math.log(x) / Math.LN10 } /** * @param {number} x */ static roundToBase10(x) { return Math.pow(10, Math.floor(Math.log10(x))) } /** * @param {number} x * @param {number} base */ static roundToBase(x, base) { return Math.pow(base, Math.floor(Math.log(x) / Math.log(base))) } /** * @param {any} val */ static parseNumber(val) { if (typeof val === 'number' || val === null) return val return parseFloat(val) } /** * @param {number} num */ static stripNumber(num, precision = 2) { return Number.isInteger(num) ? num : parseFloat(num.toPrecision(precision)) } static randomId() { return (Math.random() + 1).toString(36).substring(4) } /** * @param {number} num */ static noExponents(num) { // Check if the number contains 'e' (exponential notation) if (num.toString().includes('e')) { return Math.round(num) // Round the number } return num // Return as-is if no exponential notation } /** * @param {any} element */ static elementExists(element) { if (!element || !element.isConnected) { return false } return true } /** * detects if an element is inside a Shadow DOM * @param {any} el */ static isInShadowDOM(el) { if (!el || !el.getRootNode) { return false } const rootNode = el.getRootNode() // check if root node is a ShadowRoot return rootNode && rootNode !== document && Utils.is('ShadowRoot', rootNode) } /** * gets the shadow root host element * @param {any} el */ static getShadowRootHost(el) { if (!Utils.isInShadowDOM(el)) { return null } const rootNode = el.getRootNode() return rootNode.host || null } /** * @param {any} el */ static getDimensions(el) { if (!el) return [0, 0] // SSR: use provided dimensions or defaults if (Environment.isSSR()) { return [el._ssrWidth || 400, el._ssrHeight || 300] } let computedStyle try { computedStyle = getComputedStyle(el, null) } catch (e) { return [el.clientWidth || 0, el.clientHeight || 0] } let elementWidth = el.clientWidth let elementHeight = el.clientHeight // clientWidth/Height can be 0 when height:'100%' hasn't resolved yet // (common inside shadow DOM or detached subtrees) — fall back to BCR if (!elementWidth || !elementHeight) { const rect = el.getBoundingClientRect() elementWidth = elementWidth || rect.width elementHeight = elementHeight || rect.height } elementHeight -= parseFloat(computedStyle.paddingTop) + parseFloat(computedStyle.paddingBottom) elementWidth -= parseFloat(computedStyle.paddingLeft) + parseFloat(computedStyle.paddingRight) return [elementWidth, elementHeight] } /** * @returns {any} * @param {any} element */ static getBoundingClientRect(element) { if (!element) { return { top: 0, right: 0, bottom: 0, left: 0, width: 0, height: 0, x: 0, y: 0, } } // SSR: use abstraction layer if (Environment.isSSR()) { return BrowserAPIs.getBoundingClientRect(element) } const rect = element.getBoundingClientRect() return { top: rect.top, right: rect.right, bottom: rect.bottom, left: rect.left, width: element.clientWidth, height: element.clientHeight, x: rect.left, y: rect.top, } } /** * @param {any[]} arr */ static getLargestStringFromArr(arr) { /** * @param {string} a * @param {string} b */ return arr.reduce((a, b) => { if (Array.isArray(b)) { b = b.reduce((aa, bb) => (aa.length > bb.length ? aa : bb)) } return a.length > b.length ? a : b }, 0) } // http://stackoverflow.com/questions/5623838/rgb-to-hex-and-hex-to-rgb#answer-12342275 static hexToRgba(hex = '#999999', opacity = 0.6) { if (hex.substring(0, 1) !== '#') { hex = '#999999' } const hexStr = hex.replace('#', '') /** @type {any[]} */ const h = hexStr.match(new RegExp('(.{' + hexStr.length / 3 + '})', 'g')) || [] for (let i = 0; i < h.length; i++) { h[i] = parseInt(h[i].length === 1 ? h[i] + h[i] : h[i], 16) } if (typeof opacity !== 'undefined') h.push(opacity) return 'rgba(' + h.join(',') + ')' } /** * @param {string} rgba */ static getOpacityFromRGBA(rgba) { return parseFloat(rgba.replace(/^.*,(.+)\)/, '$1')) } /** * @param {any} rgb */ static rgb2hex(rgb) { rgb = rgb.match( /^rgba?[\s+]?\([\s+]?(\d+)[\s+]?,[\s+]?(\d+)[\s+]?,[\s+]?(\d+)[\s+]?/i, ) return rgb && rgb.length === 4 ? '#' + ('0' + parseInt(rgb[1], 10).toString(16)).slice(-2) + ('0' + parseInt(rgb[2], 10).toString(16)).slice(-2) + ('0' + parseInt(rgb[3], 10).toString(16)).slice(-2) : '' } /** * @param {number} percent * @param {string} color */ shadeRGBColor(percent, color) { const f = color.split(','), t = percent < 0 ? 0 : 255, p = percent < 0 ? percent * -1 : percent, R = parseInt(f[0].slice(4), 10), G = parseInt(f[1], 10), B = parseInt(f[2], 10) return ( 'rgb(' + (Math.round((t - R) * p) + R) + ',' + (Math.round((t - G) * p) + G) + ',' + (Math.round((t - B) * p) + B) + ')' ) } /** * @param {number} percent * @param {string} color */ shadeHexColor(percent, color) { const f = parseInt(color.slice(1), 16), t = percent < 0 ? 0 : 255, p = percent < 0 ? percent * -1 : percent, R = f >> 16, G = (f >> 8) & 0x00ff, B = f & 0x0000ff return ( '#' + ( 0x1000000 + (Math.round((t - R) * p) + R) * 0x10000 + (Math.round((t - G) * p) + G) * 0x100 + (Math.round((t - B) * p) + B) ) .toString(16) .slice(1) ) } // beautiful color shading blending code // http://stackoverflow.com/questions/5560248/programmatically-lighten-or-darken-a-hex-color-or-rgb-and-blend-colors /** * @param {number} p * @param {string} color */ shadeColor(p, color) { if (Utils.isColorHex(color)) { return this.shadeHexColor(p, color) } else { return this.shadeRGBColor(p, color) } } /** * @param {string} color */ static isColorHex(color) { return /(^#[0-9A-F]{6}$)|(^#[0-9A-F]{3}$)|(^#[0-9A-F]{8}$)/i.test(color) } /** * @param {string} color */ static isCSSVariable(color) { if (typeof color !== 'string') return false const value = color.trim() return value.startsWith('var(') && value.endsWith(')') } /** * @param {string} color */ static getThemeColor(color) { if (!Utils.isCSSVariable(color)) return color // CSS variable resolution requires a live DOM — not possible in SSR if (Environment.isSSR()) return color const tempElem = document.createElement('div') tempElem.style.cssText = 'position:fixed; left: -9999px; visibility:hidden;' tempElem.style.color = color document.body.appendChild(tempElem) let computedColor try { computedColor = window.getComputedStyle(tempElem).color } finally { if (tempElem.parentNode) { tempElem.parentNode.removeChild(tempElem) } } return computedColor } /** * @param {string} color * @param {number} opacity */ static applyOpacityToColor(color, opacity) { const value = Number(opacity) if (!Number.isFinite(value)) return color if (value <= 0) return 'transparent' if (value >= 1) return color const percent = Math.round(value * 100) return `color-mix(in srgb, ${color} ${percent}%, transparent)` } /** * @param {number} size * @param {number} dataPointsLen */ static getPolygonPos(size, dataPointsLen) { const dotsArray = [] const angle = (Math.PI * 2) / dataPointsLen for (let i = 0; i < dataPointsLen; i++) { const curPos = {} curPos.x = size * Math.sin(i * angle) curPos.y = -size * Math.cos(i * angle) dotsArray.push(curPos) } return dotsArray } /** * @param {number} centerX * @param {number} centerY * @param {number} radius * @param {number} angleInDegrees */ static polarToCartesian(centerX, centerY, radius, angleInDegrees) { const angleInRadians = ((angleInDegrees - 90) * Math.PI) / 180.0 return { x: centerX + radius * Math.cos(angleInRadians), y: centerY + radius * Math.sin(angleInRadians), } } /** * @param {string} str */ static escapeString(str, escapeWith = 'x') { let newStr = str.toString().slice() newStr = newStr.replace(/[` ~!@#$%^&*()|+=?;:'",.<>{}[\]\\/]/gi, escapeWith) return newStr } /** * @param {number} val */ static negToZero(val) { return val < 0 ? 0 : val } /** * @param {any[]} arr * @param {number} old_index * @param {number} new_index */ static moveIndexInArray(arr, old_index, new_index) { if (new_index >= arr.length) { let k = new_index - arr.length + 1 while (k--) { arr.push(undefined) } } arr.splice(new_index, 0, arr.splice(old_index, 1)[0]) return arr } /** * @param {string} s */ static extractNumber(s) { return parseFloat(s.replace(/[^\d.]*/g, '')) } /** * @param {any} el * @param {string} cls */ static findAncestor(el, cls) { while ((el = el.parentElement) && !el.classList.contains(cls)); return el } /** * @param {any} el * @param {Record<string, any>} styles */ static setELstyles(el, styles) { for (const key in styles) { if (Object.prototype.hasOwnProperty.call(styles, key)) { el.style.key = styles[key] } } } // prevents JS prevision errors when adding /** * @param {number} a * @param {number} b */ static preciseAddition(a, b) { const aDecimals = (String(a).split('.')[1] || '').length const bDecimals = (String(b).split('.')[1] || '').length const factor = Math.pow(10, Math.max(aDecimals, bDecimals)) return (Math.round(a * factor) + Math.round(b * factor)) / factor } /** * @param {any} value */ static isNumber(value) { return ( !isNaN(value) && parseFloat(String(Number(value))) === value && !isNaN(parseInt(value, 10)) ) } /** * @param {number} n */ static isFloat(n) { return Number(n) === n && n % 1 !== 0 } static isMsEdge() { if (Environment.isSSR()) return false const ua = window.navigator.userAgent const edge = ua.indexOf('Edge/') if (edge > 0) { // Edge (IE 12+) => return version number return parseInt(ua.substring(edge + 5, ua.indexOf('.', edge)), 10) } // other browser return false } // // Find the Greatest Common Divisor of two numbers // /** * @param {number} a * @param {number} b */ static getGCD(a, b, p = 7) { let factor = Math.pow(10, p - Math.floor(Math.log10(Math.max(a, b)))) if (factor > 1) { a = Math.round(Math.abs(a) * factor) b = Math.round(Math.abs(b) * factor) } else { factor = 1 } while (b) { const t = b b = a % b a = t } return a / factor } /** * @param {number} n */ static getPrimeFactors(n) { const factors = [] let divisor = 2 while (n >= 2) { if (n % divisor == 0) { factors.push(divisor) n = n / divisor } else { divisor++ } } return factors } /** * @param {number} a * @param {number} b */ static mod(a, b, p = 7) { const big = Math.pow(10, p - Math.floor(Math.log10(Math.max(a, b)))) a = Math.round(Math.abs(a) * big) b = Math.round(Math.abs(b) * big) return (a % b) / big } } export default Utils