UNPKG

@odoo/o-spreadsheet

Version:
1,667 lines (1,659 loc) 3.28 MB
/** * This file is generated by o-spreadsheet build tools. Do not edit it. * @see https://github.com/odoo/o-spreadsheet * @version 18.4.5 * @date 2025-08-04T06:54:49.107Z * @hash 358931f */ import { useEnv, useSubEnv, onWillUnmount, useComponent, status, Component, useRef, onMounted, useEffect, App, blockDom, useState, onPatched, useExternalListener, onWillUpdateProps, onWillStart, onWillPatch, xml, useChildSubEnv, markRaw, toRaw } from '@odoo/owl'; function createActions(menuItems) { return menuItems.map(createAction).sort((a, b) => a.sequence - b.sequence); } let nextItemId = 1; function createAction(item) { const name = item.name; const children = item.children; const description = item.description; const icon = item.icon; const secondaryIcon = item.secondaryIcon; const itemId = item.id || nextItemId++; const isEnabled = item.isEnabled ? item.isEnabled : () => true; return { id: itemId.toString(), name: typeof name === "function" ? name : () => name, isVisible: item.isVisible ? item.isVisible : () => true, isEnabled: isEnabled, isActive: item.isActive, execute: item.execute ? (env, isMiddleClick) => { if (isEnabled(env)) { return item.execute(env, isMiddleClick); } return undefined; } : undefined, children: children ? (env) => { return children .map((child) => (typeof child === "function" ? child(env) : child)) .flat() .map(createAction); } : () => [], isReadonlyAllowed: item.isReadonlyAllowed || false, separator: item.separator || false, icon: typeof icon === "function" ? icon : () => icon || "", iconColor: item.iconColor, secondaryIcon: typeof secondaryIcon === "function" ? secondaryIcon : () => secondaryIcon || "", description: typeof description === "function" ? description : () => description || "", textColor: item.textColor, sequence: item.sequence || 0, onStartHover: item.onStartHover, onStopHover: item.onStopHover, }; } /** * Registry * * The Registry class is basically just a mapping from a string key to an object. * It is really not much more than an object. It is however useful for the * following reasons: * * 1. it let us react and execute code when someone add something to the registry * (for example, the FunctionRegistry subclass this for this purpose) * 2. it throws an error when the get operation fails * 3. it provides a chained API to add items to the registry. */ class Registry { content = {}; /** * Add an item to the registry, you can only add if there is no item * already present in the registery with the given key * * Note that this also returns the registry, so another add method call can * be chained */ add(key, value) { if (key in this.content) { throw new Error(`${key} is already present in this registry!`); } return this.replace(key, value); } /** * Replace (or add) an item to the registry * * Note that this also returns the registry, so another add method call can * be chained */ replace(key, value) { this.content[key] = value; return this; } /** * Get an item from the registry */ get(key) { /** * Note: key in {} is ~12 times slower than {}[key]. * So, we check the absence of key only when the direct access returns * a falsy value. It's done to ensure that the registry can contains falsy values */ const content = this.content[key]; if (!content) { if (!(key in this.content)) { throw new Error(`Cannot find ${key} in this registry!`); } } return content; } /** * Check if the key is already in the registry */ contains(key) { return key in this.content; } /** * Get a list of all elements in the registry */ getAll() { return Object.values(this.content); } /** * Get a list of all keys in the registry */ getKeys() { return Object.keys(this.content); } /** * Remove an item from the registry */ remove(key) { delete this.content[key]; } } const CANVAS_SHIFT = 0.5; // Colors const HIGHLIGHT_COLOR = "#017E84"; const BACKGROUND_GRAY_COLOR = "#f5f5f5"; const BACKGROUND_HEADER_COLOR = "#F8F9FA"; const BACKGROUND_HEADER_SELECTED_COLOR = "#E8EAED"; const BACKGROUND_HEADER_ACTIVE_COLOR = "#595959"; const TEXT_HEADER_COLOR = "#666666"; const FIGURE_BORDER_COLOR = "#c9ccd2"; const SELECTION_BORDER_COLOR = "#3266ca"; const HEADER_BORDER_COLOR = "#C0C0C0"; const CELL_BORDER_COLOR = "#E2E3E3"; const BACKGROUND_CHART_COLOR = "#FFFFFF"; const DISABLED_TEXT_COLOR = "#CACACA"; const DEFAULT_COLOR_SCALE_MIDPOINT_COLOR = 0xb6d7a8; const LINK_COLOR = HIGHLIGHT_COLOR; const FILTERS_COLOR = "#188038"; const SEPARATOR_COLOR = "#E0E2E4"; const ICONS_COLOR = "#4A4F59"; const HEADER_GROUPING_BACKGROUND_COLOR = "#F5F5F5"; const HEADER_GROUPING_BORDER_COLOR = "#999"; const GRID_BORDER_COLOR = "#E2E3E3"; const FROZEN_PANE_HEADER_BORDER_COLOR = "#BCBCBC"; const FROZEN_PANE_BORDER_COLOR = "#DADFE8"; const COMPOSER_ASSISTANT_COLOR = "#9B359B"; const COLOR_TRANSPARENT = "#00000000"; const TABLE_HOVER_BACKGROUND_COLOR = "#017E8414"; const CHART_WATERFALL_POSITIVE_COLOR = "#4EA7F2"; const CHART_WATERFALL_NEGATIVE_COLOR = "#EA6175"; const CHART_WATERFALL_SUBTOTAL_COLOR = "#AAAAAA"; const GRAY_900 = "#111827"; const GRAY_300 = "#D8DADD"; const GRAY_200 = "#E7E9ED"; const GRAY_100 = "#F9FAFB"; const TEXT_BODY = "#374151"; const TEXT_BODY_MUTED = TEXT_BODY + "C2"; const TEXT_HEADING = "#111827"; const PRIMARY_BUTTON_BG = "#714B67"; const PRIMARY_BUTTON_HOVER_BG = "#624159"; const PRIMARY_BUTTON_ACTIVE_BG = "#f1edf0"; const BUTTON_BG = GRAY_200; const BUTTON_HOVER_BG = GRAY_300; const BUTTON_HOVER_TEXT_COLOR = "#111827"; const BUTTON_ACTIVE_BG = "#e6f2f3"; const BUTTON_ACTIVE_TEXT_COLOR = "#111827"; const ACTION_COLOR = HIGHLIGHT_COLOR; const ACTION_COLOR_HOVER = "#01585c"; const ALERT_WARNING_BG = "#FBEBCC"; const ALERT_WARNING_BORDER = "#F8E2B3"; const ALERT_WARNING_TEXT_COLOR = "#946D23"; const ALERT_DANGER_BG = "#D44C591A"; const ALERT_DANGER_BORDER = "#C34A41"; const ALERT_DANGER_TEXT_COLOR = "#C34A41"; const ALERT_INFO_BG = "#CDEDF1"; const ALERT_INFO_BORDER = "#98DBE2"; const ALERT_INFO_TEXT_COLOR = "#09414A"; const BADGE_SELECTED_COLOR = "#E6F2F3"; const CHART_PADDING = 20; const CHART_PADDING_BOTTOM = 10; const CHART_PADDING_TOP = 15; const CHART_TITLE_FONT_SIZE = 16; const CHART_AXIS_TITLE_FONT_SIZE = 12; const SCORECARD_CHART_TITLE_FONT_SIZE = 14; const PIVOT_TOKEN_COLOR = "#F28C28"; // Color picker defaults as upper case HEX to match `toHex`helper const COLOR_PICKER_DEFAULTS = [ "#000000", "#434343", "#666666", "#999999", "#B7B7B7", "#CCCCCC", "#D9D9D9", "#EFEFEF", "#F3F3F3", "#FFFFFF", "#980000", "#FF0000", "#FF9900", "#FFFF00", "#00FF00", "#00FFFF", "#4A86E8", "#0000FF", "#9900FF", "#FF00FF", "#E6B8AF", "#F4CCCC", "#FCE5CD", "#FFF2CC", "#D9EAD3", "#D0E0E3", "#C9DAF8", "#CFE2F3", "#D9D2E9", "#EAD1DC", "#DD7E6B", "#EA9999", "#F9CB9C", "#FFE599", "#B6D7A8", "#A2C4C9", "#A4C2F4", "#9FC5E8", "#B4A7D6", "#D5A6BD", "#CC4125", "#E06666", "#F6B26B", "#FFD966", "#93C47D", "#76A5AF", "#6D9EEB", "#6FA8DC", "#8E7CC3", "#C27BA0", "#A61C00", "#CC0000", "#E69138", "#F1C232", "#6AA84F", "#45818E", "#3C78D8", "#3D85C6", "#674EA7", "#A64D79", "#85200C", "#990000", "#B45F06", "#BF9000", "#38761D", "#134F5C", "#1155CC", "#0B5394", "#351C75", "#741B47", "#5B0F00", "#660000", "#783F04", "#7F6000", "#274E13", "#0C343D", "#1C4587", "#073763", "#20124D", "#4C1130", ]; // Dimensions const MIN_ROW_HEIGHT = 10; const MIN_COL_WIDTH = 5; const HEADER_HEIGHT = 26; const HEADER_WIDTH = 48; const DESKTOP_TOPBAR_TOOLBAR_HEIGHT = 34; const MOBILE_TOPBAR_TOOLBAR_HEIGHT = 44; const DESKTOP_BOTTOMBAR_HEIGHT = 36; const MOBILE_BOTTOMBAR_HEIGHT = 44; const DEFAULT_CELL_WIDTH = 96; const DEFAULT_CELL_HEIGHT = 23; const SCROLLBAR_WIDTH = 15; const AUTOFILL_EDGE_LENGTH = 8; const ICON_EDGE_LENGTH = 18; const MIN_CF_ICON_MARGIN = 4; const MIN_CELL_TEXT_MARGIN = 4; const CF_ICON_EDGE_LENGTH = 15; const PADDING_AUTORESIZE_VERTICAL = 3; const PADDING_AUTORESIZE_HORIZONTAL = MIN_CELL_TEXT_MARGIN; const GROUP_LAYER_WIDTH = 21; const GRID_ICON_MARGIN = 2; const GRID_ICON_EDGE_LENGTH = 17; const FOOTER_HEIGHT = 2 * DEFAULT_CELL_HEIGHT; const DATA_VALIDATION_CHIP_MARGIN = 5; // 768px is a common breakpoint for small screens // Typically inside Odoo, it is the threshold for switching to mobile view const MOBILE_WIDTH_BREAKPOINT = 768; // Menus const MENU_WIDTH = 250; const MENU_VERTICAL_PADDING = 6; const DESKTOP_MENU_ITEM_HEIGHT = 26; const MOBILE_MENU_ITEM_HEIGHT = 35; const MENU_ITEM_PADDING_HORIZONTAL = 11; const MENU_ITEM_PADDING_VERTICAL = 4; const MENU_SEPARATOR_BORDER_WIDTH = 1; const MENU_SEPARATOR_PADDING = 5; // Style const DEFAULT_STYLE = { align: "left", verticalAlign: "bottom", wrapping: "overflow", bold: false, italic: false, strikethrough: false, underline: false, fontSize: 10, fillColor: "", textColor: "", }; const DEFAULT_VERTICAL_ALIGN = DEFAULT_STYLE.verticalAlign; const DEFAULT_WRAPPING_MODE = DEFAULT_STYLE.wrapping; // Fonts const DEFAULT_FONT_WEIGHT = "400"; const DEFAULT_FONT_SIZE = DEFAULT_STYLE.fontSize; const HEADER_FONT_SIZE = 11; const DEFAULT_FONT = "'Roboto', arial"; // Borders const DEFAULT_BORDER_DESC = { style: "thin", color: "#000000" }; // Max Number of history steps kept in memory const MAX_HISTORY_STEPS = 99; // Id of the first revision const DEFAULT_REVISION_ID = "START_REVISION"; // Figure const DEFAULT_FIGURE_HEIGHT = 335; const DEFAULT_FIGURE_WIDTH = 536; const FIGURE_BORDER_WIDTH = 1; const MIN_FIG_SIZE = 80; // Chart const MAX_CHAR_LABEL = 20; const FIGURE_ID_SPLITTER = "??"; const DEFAULT_GAUGE_LOWER_COLOR = "#EA6175"; const DEFAULT_GAUGE_MIDDLE_COLOR = "#FFD86D"; const DEFAULT_GAUGE_UPPER_COLOR = "#43C5B1"; const DEFAULT_SCORECARD_BASELINE_MODE = "difference"; const DEFAULT_SCORECARD_BASELINE_COLOR_UP = "#43C5B1"; const DEFAULT_SCORECARD_BASELINE_COLOR_DOWN = "#EA6175"; const DEFAULT_SCORECARD_KEY_VALUE_FONT_SIZE = 32; const DEFAULT_SCORECARD_BASELINE_FONT_SIZE = 16; const LINE_FILL_TRANSPARENCY = 0.4; const LINE_DATA_POINT_RADIUS = 3; const DEFAULT_WINDOW_SIZE = 2; // session const DEBOUNCE_TIME = 200; const MESSAGE_VERSION = 1; // Sheets const FORBIDDEN_SHEETNAME_CHARS = ["'", "*", "?", "/", "\\", "[", "]"]; const FORBIDDEN_SHEETNAME_CHARS_IN_EXCEL_REGEX = /'|\*|\?|\/|\\|\[|\]/; // Cells const FORMULA_REF_IDENTIFIER = "|"; // Components var ComponentsImportance; (function (ComponentsImportance) { ComponentsImportance[ComponentsImportance["Grid"] = 0] = "Grid"; ComponentsImportance[ComponentsImportance["Highlight"] = 5] = "Highlight"; ComponentsImportance[ComponentsImportance["HeaderGroupingButton"] = 6] = "HeaderGroupingButton"; ComponentsImportance[ComponentsImportance["Figure"] = 10] = "Figure"; ComponentsImportance[ComponentsImportance["ScrollBar"] = 15] = "ScrollBar"; ComponentsImportance[ComponentsImportance["GridPopover"] = 19] = "GridPopover"; ComponentsImportance[ComponentsImportance["GridComposer"] = 20] = "GridComposer"; ComponentsImportance[ComponentsImportance["IconPicker"] = 25] = "IconPicker"; ComponentsImportance[ComponentsImportance["TopBarComposer"] = 30] = "TopBarComposer"; ComponentsImportance[ComponentsImportance["Popover"] = 35] = "Popover"; ComponentsImportance[ComponentsImportance["FigureAnchor"] = 1000] = "FigureAnchor"; ComponentsImportance[ComponentsImportance["FigureSnapLine"] = 1001] = "FigureSnapLine"; ComponentsImportance[ComponentsImportance["FigureTooltip"] = 1002] = "FigureTooltip"; })(ComponentsImportance || (ComponentsImportance = {})); let DEFAULT_SHEETVIEW_SIZE = 0; function getDefaultSheetViewSize() { return DEFAULT_SHEETVIEW_SIZE; } function setDefaultSheetViewSize(size) { DEFAULT_SHEETVIEW_SIZE = size; } const MAXIMAL_FREEZABLE_RATIO = 0.85; const NEWLINE = "\n"; const FONT_SIZES = [6, 7, 8, 9, 10, 11, 12, 14, 18, 24, 36]; // Pivot const PIVOT_TABLE_CONFIG = { hasFilters: false, totalRow: false, firstColumn: true, lastColumn: false, numberOfHeaders: 1, bandedRows: true, bandedColumns: false, styleId: "TableStyleMedium5", automaticAutofill: false, }; const PIVOT_INDENT = 15; const PIVOT_COLLAPSE_ICON_SIZE = 12; const DEFAULT_CURRENCY = { symbol: "$", position: "before", decimalPlaces: 2, code: "", name: "Dollar", }; //------------------------------------------------------------------------------ // Miscellaneous //------------------------------------------------------------------------------ const sanitizeSheetNameRegex = new RegExp(FORBIDDEN_SHEETNAME_CHARS_IN_EXCEL_REGEX, "g"); /** * Remove quotes from a quoted string * ```js * removeStringQuotes('"Hello"') * > 'Hello' * ``` */ function removeStringQuotes(str) { if (str[0] === '"') { str = str.slice(1); } if (str[str.length - 1] === '"' && str[str.length - 2] !== "\\") { return str.slice(0, str.length - 1); } return str; } function isCloneable(obj) { return "clone" in obj && obj.clone instanceof Function; } /** * Escapes a string to use as a literal string in a RegExp. * @url https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions#Escaping */ function escapeRegExp(str) { return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); } /** * Deep copy arrays, plain objects and primitive values. * Throws an error for other types such as class instances. * Sparse arrays remain sparse. */ function deepCopy(obj) { switch (typeof obj) { case "object": { if (obj === null) { return obj; } else if (isCloneable(obj)) { return obj.clone(); } else if (!(isPlainObject(obj) || obj instanceof Array)) { throw new Error("Unsupported type: only objects and arrays are supported"); } const result = Array.isArray(obj) ? new Array(obj.length) : {}; if (Array.isArray(obj)) { for (let i = 0, len = obj.length; i < len; i++) { if (i in obj) { result[i] = deepCopy(obj[i]); } } } else { for (const key in obj) { result[key] = deepCopy(obj[key]); } } return result; } case "number": case "string": case "boolean": case "function": case "undefined": return obj; default: throw new Error(`Unsupported type: ${typeof obj}`); } } /** * Check if the object is a plain old javascript object. */ function isPlainObject(obj) { return (typeof obj === "object" && obj !== null && // obj.constructor can be undefined when there's no prototype (`Object.create(null, {})`) (obj?.constructor === Object || obj?.constructor === undefined)); } /** * Sanitize the name of a sheet, by eventually removing quotes * @param sheetName name of the sheet, potentially quoted with single quotes */ function getUnquotedSheetName(sheetName) { return unquote(sheetName, "'"); } function unquote(string, quoteChar = '"') { if (string.startsWith(quoteChar)) { string = string.slice(1); } if (string.endsWith(quoteChar)) { string = string.slice(0, -1); } return string; } /** * Add quotes around the sheet name or any symbol name if it contains at least one non alphanumeric character * '\w' captures [0-9][a-z][A-Z] and _. * @param symbolName Name of the sheet or symbol */ function getCanonicalSymbolName(symbolName) { if (symbolName.match(/\w/g)?.length !== symbolName.length) { symbolName = `'${symbolName}'`; } return symbolName; } /** Replace the excel-excluded characters of a sheetName */ function sanitizeSheetName(sheetName, replacementChar = " ") { return sheetName.replace(sanitizeSheetNameRegex, replacementChar); } function clip(val, min, max) { return val < min ? min : val > max ? max : val; } /** * Create a range from start (included) to end (excluded). * range(10, 13) => [10, 11, 12] * range(2, 8, 2) => [2, 4, 6] */ function range(start, end, step = 1) { if (end <= start && step > 0) { return []; } if (step === 0) { throw new Error("range() step must not be zero"); } const length = Math.ceil(Math.abs((end - start) / step)); const array = Array(length); for (let i = 0; i < length; i++) { array[i] = start + i * step; } return array; } /** * Groups consecutive numbers. * The input array is assumed to be sorted * @param numbers */ function groupConsecutive(numbers) { return numbers.reduce((groups, currentRow, index, rows) => { if (Math.abs(currentRow - rows[index - 1]) === 1) { const lastGroup = groups[groups.length - 1]; lastGroup.push(currentRow); } else { groups.push([currentRow]); } return groups; }, []); } /** * Create one generator from two generators by linking * each item of the first generator to the next item of * the second generator. * * Let's say generator G1 yields A, B, C and generator G2 yields X, Y, Z. * The resulting generator of `linkNext(G1, G2)` will yield A', B', C' * where `A' = A & {next: Y}`, `B' = B & {next: Z}` and `C' = C & {next: undefined}` * @param generator * @param nextGenerator */ function* linkNext(generator, nextGenerator) { nextGenerator.next(); for (const item of generator) { const nextItem = nextGenerator.next(); yield { ...item, next: nextItem.done ? undefined : nextItem.value, }; } } function isBoolean(str) { const upperCased = str.toUpperCase(); return upperCased === "TRUE" || upperCased === "FALSE"; } const MARKDOWN_LINK_REGEX = /^\[(.+)\]\((.+)\)$/; //link must start with http or https //https://stackoverflow.com/a/3809435/4760614 const WEB_LINK_REGEX = /^https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{2,256}\.[a-z]{2,4}\b([-a-zA-Z0-9@:%_\+.~#?&//=]*)$/; function isMarkdownLink(str) { return MARKDOWN_LINK_REGEX.test(str); } /** * Check if the string is a web link. * e.g. http://odoo.com */ function isWebLink(str) { return WEB_LINK_REGEX.test(str); } /** * Build a markdown link from a label and an url */ function markdownLink(label, url) { return `[${label}](${url})`; } function parseMarkdownLink(str) { const matches = str.match(MARKDOWN_LINK_REGEX) || []; const label = matches[1]; const url = matches[2]; if (!label || !url) { throw new Error(`Could not parse markdown link ${str}.`); } return { label, url, }; } const O_SPREADSHEET_LINK_PREFIX = "o-spreadsheet://"; function isSheetUrl(url) { return url.startsWith(O_SPREADSHEET_LINK_PREFIX); } function buildSheetLink(sheetId) { return `${O_SPREADSHEET_LINK_PREFIX}${sheetId}`; } /** * Parse a sheet link and return the sheet id */ function parseSheetUrl(sheetLink) { if (sheetLink.startsWith(O_SPREADSHEET_LINK_PREFIX)) { return sheetLink.slice(O_SPREADSHEET_LINK_PREFIX.length); } throw new Error(`${sheetLink} is not a valid sheet link`); } /** * This helper function can be used as a type guard when filtering arrays. * const foo: number[] = [1, 2, undefined, 4].filter(isDefined) */ function isDefined(argument) { return argument !== undefined; } /** * Check if all the values of an object, and all the values of the objects inside of it, are undefined. */ function isObjectEmptyRecursive(argument) { if (argument === undefined) return true; return Object.values(argument).every((value) => typeof value === "object" ? isObjectEmptyRecursive(value) : !value); } /** * Returns a function, that, as long as it continues to be invoked, will not * be triggered. The function will be called after it stops being called for * N milliseconds. If `immediate` is passed, trigger the function on the * leading edge, instead of the trailing. * * Also decorate the argument function with two methods: stopDebounce and isDebouncePending. * * Inspired by https://davidwalsh.name/javascript-debounce-function */ function debounce(func, wait, immediate) { let timeout = undefined; const debounced = function () { const context = this; const args = Array.from(arguments); function later() { timeout = undefined; if (!immediate) { func.apply(context, args); } } const callNow = immediate && !timeout; clearTimeout(timeout); timeout = setTimeout(later, wait); if (callNow) { func.apply(context, args); } }; debounced.isDebouncePending = () => timeout !== undefined; debounced.stopDebounce = () => { clearTimeout(timeout); }; return debounced; } /** * Creates a batched version of a callback so that all calls to it in the same * microtick will only call the original callback once. * * @param callback the callback to batch * @returns a batched version of the original callback * * Copied from odoo/owl repo. */ function batched(callback) { let scheduled = false; return async (...args) => { if (!scheduled) { scheduled = true; await Promise.resolve(); scheduled = false; callback(...args); } }; } /* * Concatenate an array of strings. */ function concat(chars) { // ~40% faster than chars.join("") let output = ""; for (let i = 0, len = chars.length; i < len; i++) { output += chars[i]; } return output; } /** * Lazy value computed by the provided function. */ function lazy(fn) { let isMemoized = false; let memo; const lazyValue = () => { if (!isMemoized) { memo = fn instanceof Function ? fn() : fn; isMemoized = true; } return memo; }; lazyValue.map = (callback) => lazy(() => callback(lazyValue())); return lazyValue; } /** * Find the next defined value after the given index in an array of strings. If there is no defined value * after the index, return the closest defined value before the index. Return an empty string if no * defined value was found. * */ function findNextDefinedValue(arr, index) { let value = arr.slice(index).find((val) => val); if (!value) { value = arr .slice(0, index) .reverse() .find((val) => val); } return value || ""; } /** Get index of first header added by an ADD_COLUMNS_ROWS command */ function getAddHeaderStartIndex(position, base) { return position === "after" ? base + 1 : base; } /** * Compares two objects. */ function deepEquals(o1, o2) { if (o1 === o2) return true; if ((o1 && !o2) || (o2 && !o1)) return false; if (typeof o1 !== typeof o2) return false; if (typeof o1 !== "object") return false; // Objects can have different keys if the values are undefined for (const key in o2) { if (!(key in o1) && o2[key] !== undefined) { return false; } } for (const key in o1) { if (typeof o1[key] !== typeof o2[key]) return false; if (typeof o1[key] === "object") { if (!deepEquals(o1[key], o2[key])) return false; } else { if (o1[key] !== o2[key]) return false; } } return true; } /** * Compares two arrays. * For performance reasons, this function is to be preferred * to 'deepEquals' in the case we know that the inputs are arrays. */ function deepEqualsArray(arr1, arr2) { if (arr1.length !== arr2.length) { return false; } for (let i = 0; i < arr1.length; i++) { if (!deepEquals(arr1[i], arr2[i])) { return false; } } return true; } /** * Check if the given array contains all the values of the other array. * It makes the assumption that both array do not contain duplicates. */ function includesAll(arr, values) { if (arr.length < values.length) { return false; } const set = new Set(arr); return values.every((value) => set.has(value)); } /** * Return an object with all the keys in the object that have a falsy value removed. */ function removeFalsyAttributes(obj) { if (!obj) return obj; const cleanObject = { ...obj }; Object.keys(cleanObject).forEach((key) => !cleanObject[key] && delete cleanObject[key]); return cleanObject; } /** * Equivalent to "\s" in regexp, minus the new lines characters * * https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions/Character_Classes */ const specialWhiteSpaceSpecialCharacters = [ "\t", "\f", "\v", String.fromCharCode(parseInt("00a0", 16)), String.fromCharCode(parseInt("1680", 16)), String.fromCharCode(parseInt("2000", 16)), String.fromCharCode(parseInt("200a", 16)), String.fromCharCode(parseInt("2028", 16)), String.fromCharCode(parseInt("2029", 16)), String.fromCharCode(parseInt("202f", 16)), String.fromCharCode(parseInt("205f", 16)), String.fromCharCode(parseInt("3000", 16)), String.fromCharCode(parseInt("feff", 16)), ]; const specialWhiteSpaceRegexp = new RegExp(specialWhiteSpaceSpecialCharacters.join("|"), "g"); const newLineRegexp = /(\r\n|\r)/g; const whiteSpaceCharacters = specialWhiteSpaceSpecialCharacters.concat([" "]); /** * Replace all different newlines characters by \n */ function replaceNewLines(text) { if (!text) return ""; return text.replace(newLineRegexp, NEWLINE); } /** * Determine if the numbers are consecutive. */ function isConsecutive(iterable) { const array = Array.from(iterable).sort((a, b) => a - b); // sort numerically rather than lexicographically for (let i = 1; i < array.length; i++) { if (array[i] - array[i - 1] !== 1) { return false; } } return true; } /** * Creates a version of the function that's memoized on the value of its first * argument, if any. */ function memoize(func) { const cache = new Map(); const funcName = func.name ? func.name + " (memoized)" : "memoized"; return { [funcName](...args) { if (!cache.has(args[0])) { cache.set(args[0], func(...args)); } return cache.get(args[0]); }, }[funcName]; } function removeIndexesFromArray(array, indexes) { return array.filter((_, index) => !indexes.includes(index)); } function insertItemsAtIndex(array, items, index) { const newArray = [...array]; newArray.splice(index, 0, ...items); return newArray; } function replaceItemAtIndex(array, newItem, index) { const newArray = [...array]; newArray[index] = newItem; return newArray; } function trimContent(content) { const contentLines = content.split("\n"); return contentLines.map((line) => line.replace(/\s+/g, " ").trim()).join("\n"); } function isNumberBetween(value, min, max) { if (min > max) { return isNumberBetween(value, max, min); } return value >= min && value <= max; } /** * Get a Regex for the find & replace that matches the given search string and options. */ function getSearchRegex(searchStr, searchOptions) { let searchValue = escapeRegExp(searchStr); const flags = !searchOptions.matchCase ? "i" : ""; if (searchOptions.exactMatch) { searchValue = `^${searchValue}$`; } return RegExp(searchValue, flags); } /** * Alternative to Math.max that works with large arrays. * Typically useful for arrays bigger than 100k elements. */ function largeMax(array) { let len = array.length; if (len < 100_000) return Math.max(...array); let max = -Infinity; while (len--) { max = array[len] > max ? array[len] : max; } return max; } /** * Alternative to Math.min that works with large arrays. * Typically useful for arrays bigger than 100k elements. */ function largeMin(array) { let len = array.length; if (len < 100_000) return Math.min(...array); let min = +Infinity; while (len--) { min = array[len] < min ? array[len] : min; } return min; } class TokenizingChars { text; currentIndex = 0; current; constructor(text) { this.text = text; this.current = text[0]; } shift() { const current = this.current; const next = this.text[++this.currentIndex]; this.current = next; return current; } advanceBy(length) { this.currentIndex += length; this.current = this.text[this.currentIndex]; } isOver() { return this.currentIndex >= this.text.length; } remaining() { return this.text.substring(this.currentIndex); } currentStartsWith(str) { if (this.current !== str[0]) { return false; } for (let j = 1; j < str.length; j++) { if (this.text[this.currentIndex + j] !== str[j]) { return false; } } return true; } } /** * Remove duplicates from an array. * * @param array The array to remove duplicates from. * @param cb A callback to get an element value. */ function removeDuplicates$1(array, cb = (a) => a) { const set = new Set(); return array.filter((item) => { const key = cb(item); if (set.has(key)) { return false; } set.add(key); return true; }); } /** * Similar to transposing and array, but with POJOs instead of arrays. Useful, for example, when manipulating * a POJO grid[col][row] and you want to transpose it to grid[row][col]. * * The resulting object is created such as result[key1][key2] = pojo[key2][key1] */ function transpose2dPOJO(pojo) { const result = {}; for (const key in pojo) { for (const subKey in pojo[key]) { if (!result[subKey]) { result[subKey] = {}; } result[subKey][key] = pojo[key][subKey]; } } return result; } function getUniqueText(text, texts, options = {}) { const compute = options.compute ?? ((text, i) => `${text} (${i})`); const computeFirstOne = options.computeFirstOne ?? false; let i = options.start ?? 1; let newText = computeFirstOne ? compute(text, i) : text; while (texts.includes(newText)) { newText = compute(text, i++); } return newText; } function isFormula(content) { return content.startsWith("=") || content.startsWith("+"); } const RBA_REGEX = /rgba?\(|\s+|\)/gi; const HEX_MATCH = /^#([A-F\d]{2}){3,4}$/; const colors = [ "#eb6d00", "#0074d9", "#ad8e00", "#169ed4", "#b10dc9", "#00a82d", "#00a3a3", "#f012be", "#3d9970", "#111111", "#62A300", "#ff4136", "#949494", "#85144b", "#001f3f", ]; /* * transform a color number (R * 256^2 + G * 256 + B) into classic hex (+alpha) value * */ function colorNumberToHex(color, alpha = 1) { const alphaHex = alpha !== 1 ? Math.round(alpha * 255) .toString(16) .padStart(2, "0") : ""; return toHex(color.toString(16).padStart(6, "0")) + alphaHex; } function colorToNumber(color) { if (typeof color === "number") { return color; } return Number.parseInt(toHex(color).slice(1, 7), 16); } /** * Converts any CSS color value to a standardized hex6 value. * Accepts: hex3, hex6, hex8, rgb[1] and rgba[1]. * * [1] under the form rgb(r, g, b, a?) or rgba(r, g, b, a?) * with r,g,b ∈ [0, 255] and a ∈ [0, 1] * * toHex("#ABC") * >> "#AABBCC" * * toHex("#AAAFFF") * >> "#AAAFFF" * * toHex("rgb(30, 80, 16)") * >> "#1E5010" * * * toHex("rgb(30, 80, 16, 0.5)") * >> "#1E501080" * */ function toHex(color) { let hexColor = color; if (color.startsWith("rgb")) { hexColor = rgbaStringToHex(color); } else { hexColor = color.replace("#", "").toUpperCase(); if (hexColor.length === 3 || hexColor.length === 4) { hexColor = hexColor.split("").reduce((acc, h) => acc + h + h, ""); } hexColor = `#${hexColor}`; } if (!HEX_MATCH.test(hexColor)) { throw new Error(`invalid color input: ${color}`); } return hexColor; } function isColorValid(color) { try { toHex(color); return true; } catch (error) { return false; } } function isHSLAValid(color) { try { hslaToHex(color); return true; } catch (error) { return false; } } const isColorValueValid = (v) => v >= 0 && v <= 255; function rgba(r, g, b, a = 1) { const isInvalid = !isColorValueValid(r) || !isColorValueValid(g) || !isColorValueValid(b) || a < 0 || a > 1; if (isInvalid) { throw new Error(`Invalid RGBA values ${[r, g, b, a]}`); } return { a, b, g, r }; } /** * The relative brightness of a point in the colorspace, normalized to 0 for * darkest black and 1 for lightest white. * https://www.w3.org/TR/WCAG20/#relativeluminancedef */ function relativeLuminance(color) { let { r, g, b } = colorToRGBA(color); r /= 255; g /= 255; b /= 255; const toLinearValue = (c) => (c <= 0.03928 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4); const R = toLinearValue(r); const G = toLinearValue(g); const B = toLinearValue(b); return 0.2126 * R + 0.7152 * G + 0.0722 * B; } /** * Convert a CSS rgb color string to a standardized hex6 color value. * * rgbaStringToHex("rgb(30, 80, 16)") * >> "#1E5010" * * rgbaStringToHex("rgba(30, 80, 16, 0.5)") * >> "#1E501080" * * DOES NOT SUPPORT NON INTEGER RGB VALUES */ function rgbaStringToHex(color) { const stringVals = color.replace(RBA_REGEX, "").split(","); let alphaHex = 255; if (stringVals.length !== 3 && stringVals.length !== 4) { throw new Error("invalid color"); } else if (stringVals.length === 4) { const alpha = parseFloat(stringVals.pop() || "1"); if (isNaN(alpha)) { throw new Error("invalid alpha value"); } alphaHex = Math.round(alpha * 255); } const vals = stringVals.map((val) => parseInt(val, 10)); if (alphaHex !== 255) { vals.push(alphaHex); } return "#" + concat(vals.map((value) => value.toString(16).padStart(2, "0"))).toUpperCase(); } /** * RGBA to HEX representation (#RRGGBBAA). * * https://css-tricks.com/converting-color-spaces-in-javascript/ */ function rgbaToHex(rgba) { let r = rgba.r.toString(16); let g = rgba.g.toString(16); let b = rgba.b.toString(16); let a = Math.round(rgba.a * 255).toString(16); if (r.length === 1) r = "0" + r; if (g.length === 1) g = "0" + g; if (b.length === 1) b = "0" + b; if (a.length === 1) a = "0" + a; if (a === "ff") a = ""; return ("#" + r + g + b + a).toUpperCase(); } /** * Color string to RGBA representation */ function colorToRGBA(color) { color = toHex(color); let r; let g; let b; let a; if (color.length === 7) { r = parseInt(color[1] + color[2], 16); g = parseInt(color[3] + color[4], 16); b = parseInt(color[5] + color[6], 16); a = 255; } else if (color.length === 9) { r = parseInt(color[1] + color[2], 16); g = parseInt(color[3] + color[4], 16); b = parseInt(color[5] + color[6], 16); a = parseInt(color[7] + color[8], 16); } else { throw new Error("Invalid color"); } a = +(a / 255).toFixed(3); return { a, r, g, b }; } /** * HSLA to RGBA. * * https://css-tricks.com/converting-color-spaces-in-javascript/ */ function hslaToRGBA(hsla) { hsla = { ...hsla }; // Must be fractions of 1 hsla.s /= 100; hsla.l /= 100; const c = (1 - Math.abs(2 * hsla.l - 1)) * hsla.s; const x = c * (1 - Math.abs(((hsla.h / 60) % 2) - 1)); const m = hsla.l - c / 2; let r = 0; let g = 0; let b = 0; if (0 <= hsla.h && hsla.h < 60) { r = c; g = x; b = 0; } else if (60 <= hsla.h && hsla.h < 120) { r = x; g = c; b = 0; } else if (120 <= hsla.h && hsla.h < 180) { r = 0; g = c; b = x; } else if (180 <= hsla.h && hsla.h < 240) { r = 0; g = x; b = c; } else if (240 <= hsla.h && hsla.h < 300) { r = x; g = 0; b = c; } else if (300 <= hsla.h && hsla.h < 360) { r = c; g = 0; b = x; } r = Math.round((r + m) * 255); g = Math.round((g + m) * 255); b = Math.round((b + m) * 255); return { a: hsla.a, r, g, b }; } /** * HSLA to RGBA. * * https://css-tricks.com/converting-color-spaces-in-javascript/ */ function rgbaToHSLA(rgba) { // Make r, g, and b fractions of 1 const r = rgba.r / 255; const g = rgba.g / 255; const b = rgba.b / 255; // Find greatest and smallest channel values const cMin = Math.min(r, g, b); const cMax = Math.max(r, g, b); const delta = cMax - cMin; let h = 0; let s = 0; let l = 0; // Calculate hue // No difference if (delta === 0) h = 0; // Red is max else if (cMax === r) h = ((g - b) / delta) % 6; // Green is max else if (cMax === g) h = (b - r) / delta + 2; // Blue is max else h = (r - g) / delta + 4; h = Math.round(h * 60); // Make negative hues positive behind 360° if (h < 0) h += 360; l = (cMax + cMin) / 2; // Calculate saturation s = delta === 0 ? 0 : delta / (1 - Math.abs(2 * l - 1)); // Multiply l and s by 100 s = +(s * 100).toFixed(1); l = +(l * 100).toFixed(1); return { a: rgba.a, h, s, l }; } function hslaToHex(hsla) { return rgbaToHex(hslaToRGBA(hsla)); } function hexToHSLA(hex) { return rgbaToHSLA(colorToRGBA(hex)); } function colorOrNumberToRGBA(color) { if (typeof color === "number") { return colorToRGBA(colorNumberToHex(color)); } return colorToRGBA(color); } /** * Will compare two color strings * A tolerance can be provided to account for small differences that could * be introduced by non-bijective transformations between color spaces. * * E.g. HSV <-> RGB is not a bijection * * Note that the tolerance is applied on the euclidean distance between * the two **normalized** color values. */ function isSameColor(color1, color2, tolerance = 0) { if (!(isColorValid(color1) && isColorValid(color2))) { return false; } const rgb1 = colorToRGBA(color1); const rgb2 = colorToRGBA(color2); // alpha cannot differ as it is not impacted by transformations if (rgb1.a !== rgb2.a) { return false; } const diff = Math.sqrt(((rgb1.r - rgb2.r) / 255) ** 2 + ((rgb1.g - rgb2.g) / 255) ** 2 + ((rgb1.b - rgb2.b) / 255) ** 2); return diff <= tolerance; } function setColorAlpha(color, alpha) { return alpha === 1 ? toHex(color).slice(0, 7) : rgbaToHex({ ...colorToRGBA(color), a: alpha }); } function lightenColor(color, percentage) { const hsla = hexToHSLA(color); if (percentage === 1) { return "#fff"; } hsla.l = percentage * (100 - hsla.l) + hsla.l; return hslaToHex(hsla); } function darkenColor(color, percentage) { const hsla = hexToHSLA(color); if (percentage === 1) { return "#000"; } // increase saturation to compensate and make it more vivid hsla.s = Math.min(100, percentage * hsla.s + hsla.s); hsla.l = hsla.l - percentage * hsla.l; return hslaToHex(hsla); } function chipTextColor(chipBackgroundColor) { return relativeLuminance(chipBackgroundColor) < 0.6 ? lightenColor(chipBackgroundColor, 0.9) : darkenColor(chipBackgroundColor, 0.75); } const COLORS_SM = [ "#4EA7F2", // Blue "#EA6175", // Red "#43C5B1", // Teal "#F4A261", // Orange "#8481DD", // Purple "#FFD86D", // Yellow ]; const COLORS_MD = [ "#4EA7F2", // Blue #1 "#3188E6", // Blue #2 "#43C5B1", // Teal #1 "#00A78D", // Teal #2 "#EA6175", // Red #1 "#CE4257", // Red #2 "#F4A261", // Orange #1 "#F48935", // Orange #2 "#8481DD", // Purple #1 "#5752D1", // Purple #2 "#FFD86D", // Yellow #1 "#FFBC2C", // Yellow #2 ]; const COLORS_LG = [ "#4EA7F2", // Blue #1 "#3188E6", // Blue #2 "#056BD9", // Blue #3 "#A76DBC", // Violet #1 "#7F4295", // Violet #2 "#6D2387", // Violet #3 "#EA6175", // Red #1 "#CE4257", // Red #2 "#982738", // Red #3 "#43C5B1", // Teal #1 "#00A78D", // Teal #2 "#0E8270", // Teal #3 "#F4A261", // Orange #1 "#F48935", // Orange #2 "#BE5D10", // Orange #3 "#8481DD", // Purple #1 "#5752D1", // Purple #2 "#3A3580", // Purple #3 "#A4A8B6", // Gray #1 "#7E8290", // Gray #2 "#545B70", // Gray #3 "#FFD86D", // Yellow #1 "#FFBC2C", // Yellow #2 "#C08A16", // Yellow #3 ]; const COLORS_XL = [ "#4EA7F2", // Blue #1 "#3188E6", // Blue #2 "#056BD9", // Blue #3 "#155193", // Blue #4 "#A76DBC", // Violet #1 "#7F4295", // Violet #2 "#6D2387", // Violet #3 "#4F1565", // Violet #4 "#EA6175", // Red #1 "#CE4257", // Red #2 "#982738", // Red #3 "#791B29", // Red #4 "#43C5B1", // Teal #1 "#00A78D", // Teal #2 "#0E8270", // Teal #3 "#105F53", // Teal #4 "#F4A261", // Orange #1 "#F48935", // Orange #2 "#BE5D10", // Orange #3 "#7D380D", // Orange #4 "#8481DD", // Purple #1 "#5752D1", // Purple #2 "#3A3580", // Purple #3 "#26235F", // Purple #4 "#A4A8B6", // Grey #1 "#7E8290", // Grey #2 "#545B70", // Grey #3 "#3F4250", // Grey #4 "#FFD86D", // Yellow #1 "#FFBC2C", // Yellow #2 "#C08A16", // Yellow #3 "#936A12", // Yellow #4 ]; // Same as above but with alternating colors const ALTERNATING_COLORS_MD = [ "#4EA7F2", // Blue #1 "#43C5B1", // Teal #1 "#EA6175", // Red #1 "#F4A261", // Orange #1 "#8481DD", // Purple #1 "#FFD86D", // Yellow #1 "#3188E6", // Blue #2 "#00A78D", // Teal #2 "#CE4257", // Red #2 "#F48935", // Orange #2 "#5752D1", // Purple #2 "#FFBC2C", // Yellow #2 ]; const ALTERNATING_COLORS_LG = [ "#4EA7F2", // Blue #1 "#A76DBC", // Violet #1 "#EA6175", // Red #1 "#43C5B1", // Teal #1 "#F4A261", // Orange #1 "#8481DD", // Purple #1 "#A4A8B6", // Gray #1 "#FFD86D", // Yellow #1 "#3188E6", // Blue #2 "#7F4295", // Violet #2 "#CE4257", // Red #2 "#00A78D", // Teal #2 "#F48935", // Orange #2 "#5752D1", // Purple #2 "#7E8290", // Gray #2 "#FFBC2C", // Yellow #2 "#056BD9", // Blue #3 "#6D2387", // Violet #3 "#982738", // Red #3 "#0E8270", // Teal #3 "#BE5D10", // Orange #3 "#3A3580", // Purple #3 "#545B70", // Gray #3 "#C08A16", // Yellow #3 ]; const ALTERNATING_COLORS_XL = [ "#4EA7F2", // Blue #1 "#A76DBC", // Violet #1 "#EA6175", // Red #1 "#43C5B1", // Teal #1 "#F4A261", // Orange #1 "#8481DD", // Purple #1 "#A4A8B6", // Grey #1 "#FFD86D", // Yellow #1 "#3188E6", // Blue #2 "#7F4295", // Violet #2 "#CE4257", // Red #2 "#00A78D", // Teal #2 "#F48935", // Orange #2 "#5752D1", // Purple #2 "#7E8290", // Grey #2 "#FFBC2C", // Yellow #2 "#056BD9", // Blue #3 "#6D2387", // Violet #3 "#982738", // Red #3 "#0E8270", // Teal #3 "#BE5D10", // Orange #3 "#3A3580", // Purple #3 "#545B70", // Grey #3 "#C08A16", // Yellow #3 "#155193", // Blue #4 "#4F1565", // Violet #4 "#791B29", // Red #4 "#105F53", // Teal #4 "#7D380D", // Orange #4 "#26235F", // Purple #4 "#3F4250", // Grey #4 "#936A12", // Yellow #4 ]; function getNthColor(index, palette) { return palette[index % palette.length]; } function getColorsPalette(quantity) { if (quantity <= 6) { return COLORS_SM; } else if (quantity <= 12) { return COLORS_MD; } else if (quantity <= 24) { return COLORS_LG; } else { return COLORS_XL; } } function getAlternatingColorsPalette(quantity) { if (quantity <= 6) { return COLORS_SM; } else if (quantity <= 12) { return ALTERNATING_COLORS_MD; } else if (quantity <= 24) { return ALTERNATING_COLORS_LG; } else { return ALTERNATING_COLORS_XL; } } class ColorGenerator { preferredColors; currentColorIndex = 0; palette; constructor(paletteSize, preferredColors = []) { this.preferredColors = preferredColors; this.palette = getColorsPalette(paletteSize).filter((c) => !preferredColors.includes(c)); } next() { return this.preferredColors?.[this.currentColorIndex] ? this.preferredColors[this.currentColorIndex++] : getNthColor(this.currentColorIndex++, this.palette); } } class AlternatingColorGenerator extends ColorGenerator { constructor(paletteSize, preferredColors = []) { super(paletteSize, preferredColors); this.palette = getAlternatingColorsPalette(paletteSize).filter((c) => !preferredColors.includes(c)); } } class AlternatingColorMap { availableColors; colors = {}; constructor(paletteSize = 12) { this.availableColors = new AlternatingColorGenerator(paletteSize); } get(id) { if (!this.colors[id]) { this.colors[id] = this.availableColors.next(); } return this.colors[id]; } } /** * Returns a function that maps a value to a color using a color scale defined by the given * color/threshold values pairs. */ function getColorScale(colorScalePoints) { if (colorScalePoints.length < 2) { throw new Error("Color scale must have at least 2 points"); } const sortedColorScalePoints = [...colorScalePoints.sort((a, b) => a.value - b.value)]; const thresholds = []; for (let i = 1; i < sortedColorScalePoints.length; i++) { const minColorAlpha = colorOrNumberToRGBA(sortedColorScalePoints[i - 1].color).a; const maxColorAlpha = colorOrNumberToRGBA(sortedColorScalePoints[i].color).a; const minColor = colorToNumber(sortedColorScalePoints[i - 1].color); const maxColor = colorToNumber(sortedColorScalePoints[i].color); thresholds.push({ min: sortedColorScalePoints[i - 1].value, max: sortedColorScalePoints[i].value, minColor, maxColor, minColorAlpha: minColorAlpha, maxColorAlpha: maxColorAlpha, colorDiff: computeColorDiffUnits(sortedColorScalePoints[i - 1].value, sortedColorScalePoints[i].value, minColor, maxColor), }); } return (value) => { if (value < thresholds[0].min) { return colorNumberToHex(thresholds[0].minColor, thresholds[0].minColorAlpha); } for (const threshold of thresholds) { if (value >= threshold.min && value <= threshold.max) { return colorNumberToHex(colorCell(value, threshold.min, threshold.minColor, threshold.colorDiff), threshold.maxColorAlpha); } } return colorNumberToHex(thresholds[thresholds.length - 1].maxColor, thresholds[thresholds.length - 1].maxColorAlpha); }; } function computeColorDiffUnits(minValue, maxValue, minColor, maxColor) { const deltaValue = maxValue - minValue; const deltaColorR = ((minColor >> 16) % 256) - ((maxColor >> 16) % 256); const deltaColorG = ((minColor >> 8) % 256) - ((maxColor >> 8) % 256); const deltaColorB = (minColor % 256) - (maxColor % 256); const colorDiffUnitR = deltaColorR / deltaValue; const colorDiffUnitG = deltaColorG / deltaValue; const colorDiffUnitB = deltaColorB / deltaValue; return [colorDiffUnitR, colorDiffUnitG, colorDiffUnitB]; } function colorCell(value, minValue, minColor, colorDiffUnit) { const [colorDiffUnitR, colorDiffUnitG, colorDiffUnitB] = colorDiffUnit; const r = Math.round(((minColor >> 16) % 256) - colorDiffUnitR * (value - minValue)); const g = Math.round(((minColor >> 8) % 256) - colorDiffUnitG * (value - minValue)); const b = Math.round((minColor % 256) - colorDiffUnitB * (value - minValue)); return (r << 16) | (g << 8) | b; } //------------------------------------------------------------------------------ // Coordinate //------------------------------------------------------------------------------ /** * Convert a (col) number to the corresponding le