@odoo/o-spreadsheet
Version:
A spreadsheet component
1,610 lines (1,603 loc) • 3.44 MB
JavaScript
/**
* 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
*/
(function (exports, owl) {
'use strict';
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 va