UNPKG

vis-timeline

Version:

Create a fully customizable, interactive timeline with items and ranges.

1 lines 1.27 MB
{"version":3,"file":"vis-timeline-graph2d.cjs","sources":["../../lib/DOMutil.js","../../lib/util.js","../../lib/timeline/Stack.js","../../lib/timeline/component/Group.js","../../lib/timeline/component/BackgroundGroup.js","../../lib/timeline/component/Component.js","../../lib/module/moment.js","../../lib/timeline/locales.js","../../lib/timeline/component/CurrentTime.js","../../lib/module/hammer.js","../../lib/timeline/component/CustomTime.js","../../lib/timeline/component/DataScale.js","../../lib/timeline/component/DataAxis.js","../../lib/timeline/component/graph2d_types/points.js","../../lib/timeline/component/graph2d_types/bar.js","../../lib/timeline/component/graph2d_types/line.js","../../lib/timeline/component/GraphGroup.js","../../lib/timeline/component/item/Item.js","../../lib/timeline/component/item/RangeItem.js","../../lib/timeline/component/item/BackgroundItem.js","../../lib/timeline/component/item/BoxItem.js","../../lib/timeline/component/item/ClusterItem.js","../../lib/timeline/component/item/PointItem.js","../../lib/shared/Popup.js","../../lib/timeline/DateUtil.js","../../lib/timeline/TimeStep.js","../../lib/timeline/component/ClusterGenerator.js","../../lib/timeline/component/ItemSet.js","../../lib/timeline/component/Legend.js","../../lib/timeline/component/LineGraph.js","../../lib/timeline/component/TimeAxis.js","../../lib/hammerUtil.js","../../lib/shared/Activator.js","../../lib/timeline/Core.js","../../lib/shared/ColorPicker.js","../../lib/shared/Configurator.js","../../lib/shared/Validator.js","../../lib/timeline/optionsGraph2d.js","../../lib/timeline/Range.js","../../lib/timeline/Graph2d.js","../../lib/timeline/optionsTimeline.js","../../lib/timeline/Timeline.js","../../lib/entry-esnext.js"],"sourcesContent":["// DOM utility methods\n\n/**\n * this prepares the JSON container for allocating SVG elements\n * @param {Object} JSONcontainer\n * @private\n */\nexport function prepareElements(JSONcontainer) {\n // cleanup the redundant svgElements;\n for (var elementType in JSONcontainer) {\n if (!Object.prototype.hasOwnProperty.call(JSONcontainer, elementType))\n continue;\n JSONcontainer[elementType].redundant = JSONcontainer[elementType].used;\n JSONcontainer[elementType].used = [];\n }\n}\n\n/**\n * this cleans up all the unused SVG elements. By asking for the parentNode, we only need to supply the JSON container from\n * which to remove the redundant elements.\n *\n * @param {Object} JSONcontainer\n * @private\n */\nexport function cleanupElements(JSONcontainer) {\n // cleanup the redundant svgElements;\n for (var elementType in JSONcontainer) {\n if (!Object.prototype.hasOwnProperty.call(JSONcontainer, elementType))\n continue;\n const elementTypeJsonContainer = JSONcontainer[elementType];\n for (var i = 0; i < elementTypeJsonContainer.redundant.length; i++) {\n elementTypeJsonContainer.redundant[i].parentNode.removeChild(\n elementTypeJsonContainer.redundant[i],\n );\n }\n elementTypeJsonContainer.redundant = [];\n }\n}\n\n/**\n * Ensures that all elements are removed first up so they can be recreated cleanly\n * @param {Object} JSONcontainer\n */\nexport function resetElements(JSONcontainer) {\n prepareElements(JSONcontainer);\n cleanupElements(JSONcontainer);\n prepareElements(JSONcontainer);\n}\n\n/**\n * Allocate or generate an SVG element if needed. Store a reference to it in the JSON container and draw it in the svgContainer\n * the JSON container and the SVG container have to be supplied so other svg containers (like the legend) can use this.\n *\n * @param {string} elementType\n * @param {Object} JSONcontainer\n * @param {Object} svgContainer\n * @returns {Element}\n * @private\n */\nexport function getSVGElement(elementType, JSONcontainer, svgContainer) {\n var element;\n // allocate SVG element, if it doesnt yet exist, create one.\n if (Object.prototype.hasOwnProperty.call(JSONcontainer, elementType)) {\n // this element has been created before\n // check if there is an redundant element\n if (JSONcontainer[elementType].redundant.length > 0) {\n element = JSONcontainer[elementType].redundant[0];\n JSONcontainer[elementType].redundant.shift();\n } else {\n // create a new element and add it to the SVG\n element = document.createElementNS(\n \"http://www.w3.org/2000/svg\",\n elementType,\n );\n svgContainer.appendChild(element);\n }\n } else {\n // create a new element and add it to the SVG, also create a new object in the svgElements to keep track of it.\n element = document.createElementNS(\n \"http://www.w3.org/2000/svg\",\n elementType,\n );\n JSONcontainer[elementType] = { used: [], redundant: [] };\n svgContainer.appendChild(element);\n }\n JSONcontainer[elementType].used.push(element);\n return element;\n}\n\n/**\n * Allocate or generate an SVG element if needed. Store a reference to it in the JSON container and draw it in the svgContainer\n * the JSON container and the SVG container have to be supplied so other svg containers (like the legend) can use this.\n *\n * @param {string} elementType\n * @param {Object} JSONcontainer\n * @param {Element} DOMContainer\n * @param {Element} insertBefore\n * @returns {*}\n */\nexport function getDOMElement(\n elementType,\n JSONcontainer,\n DOMContainer,\n insertBefore,\n) {\n var element;\n // allocate DOM element, if it doesnt yet exist, create one.\n if (Object.prototype.hasOwnProperty.call(JSONcontainer, elementType)) {\n // this element has been created before\n // check if there is an redundant element\n if (JSONcontainer[elementType].redundant.length > 0) {\n element = JSONcontainer[elementType].redundant[0];\n JSONcontainer[elementType].redundant.shift();\n } else {\n // create a new element and add it to the SVG\n element = document.createElement(elementType);\n if (insertBefore !== undefined) {\n DOMContainer.insertBefore(element, insertBefore);\n } else {\n DOMContainer.appendChild(element);\n }\n }\n } else {\n // create a new element and add it to the SVG, also create a new object in the svgElements to keep track of it.\n element = document.createElement(elementType);\n JSONcontainer[elementType] = { used: [], redundant: [] };\n if (insertBefore !== undefined) {\n DOMContainer.insertBefore(element, insertBefore);\n } else {\n DOMContainer.appendChild(element);\n }\n }\n JSONcontainer[elementType].used.push(element);\n return element;\n}\n\n/**\n * Draw a point object. This is a separate function because it can also be called by the legend.\n * The reason the JSONcontainer and the target SVG svgContainer have to be supplied is so the legend can use these functions\n * as well.\n *\n * @param {number} x\n * @param {number} y\n * @param {Object} groupTemplate: A template containing the necessary information to draw the datapoint e.g., {style: 'circle', size: 5, className: 'className' }\n * @param {Object} JSONcontainer\n * @param {Object} svgContainer\n * @param {Object} labelObj\n * @returns {vis.PointItem}\n */\nexport function drawPoint(\n x,\n y,\n groupTemplate,\n JSONcontainer,\n svgContainer,\n labelObj,\n) {\n var point;\n if (groupTemplate.style == \"circle\") {\n point = getSVGElement(\"circle\", JSONcontainer, svgContainer);\n point.setAttributeNS(null, \"cx\", x);\n point.setAttributeNS(null, \"cy\", y);\n point.setAttributeNS(null, \"r\", 0.5 * groupTemplate.size);\n } else {\n point = getSVGElement(\"rect\", JSONcontainer, svgContainer);\n point.setAttributeNS(null, \"x\", x - 0.5 * groupTemplate.size);\n point.setAttributeNS(null, \"y\", y - 0.5 * groupTemplate.size);\n point.setAttributeNS(null, \"width\", groupTemplate.size);\n point.setAttributeNS(null, \"height\", groupTemplate.size);\n }\n\n if (groupTemplate.styles !== undefined) {\n point.setAttributeNS(null, \"style\", groupTemplate.styles);\n }\n point.setAttributeNS(null, \"class\", groupTemplate.className + \" vis-point\");\n //handle label\n\n if (labelObj) {\n var label = getSVGElement(\"text\", JSONcontainer, svgContainer);\n if (labelObj.xOffset) {\n x = x + labelObj.xOffset;\n }\n\n if (labelObj.yOffset) {\n y = y + labelObj.yOffset;\n }\n if (labelObj.content) {\n label.textContent = labelObj.content;\n }\n\n if (labelObj.className) {\n label.setAttributeNS(null, \"class\", labelObj.className + \" vis-label\");\n }\n label.setAttributeNS(null, \"x\", x);\n label.setAttributeNS(null, \"y\", y);\n }\n\n return point;\n}\n\n/**\n * draw a bar SVG element centered on the X coordinate\n *\n * @param {number} x\n * @param {number} y\n * @param {number} width\n * @param {number} height\n * @param {string} className\n * @param {Object} JSONcontainer\n * @param {Object} svgContainer\n * @param {string} style\n */\nexport function drawBar(\n x,\n y,\n width,\n height,\n className,\n JSONcontainer,\n svgContainer,\n style,\n) {\n if (height != 0) {\n if (height < 0) {\n height *= -1;\n y -= height;\n }\n var rect = getSVGElement(\"rect\", JSONcontainer, svgContainer);\n rect.setAttributeNS(null, \"x\", x - 0.5 * width);\n rect.setAttributeNS(null, \"y\", y);\n rect.setAttributeNS(null, \"width\", width);\n rect.setAttributeNS(null, \"height\", height);\n rect.setAttributeNS(null, \"class\", className);\n if (style) {\n rect.setAttributeNS(null, \"style\", style);\n }\n }\n}\n\n/**\n * get default language\n * @returns {string}\n */\nexport function getNavigatorLanguage() {\n try {\n if (!navigator) return \"en\";\n if (navigator.languages && navigator.languages.length) {\n return navigator.languages;\n } else {\n return (\n navigator.userLanguage ||\n navigator.language ||\n navigator.browserLanguage ||\n \"en\"\n );\n }\n } catch {\n return \"en\";\n }\n}\n","// utility functions\n\nexport * from \"vis-util/esnext\";\nimport moment from \"moment\";\nimport {\n DataSet,\n createNewDataPipeFrom,\n isDataViewLike as isDataViewLikeUpstream,\n} from \"vis-data/esnext\";\nimport * as util from \"vis-util/esnext\";\nimport { getType, isNumber, isString } from \"vis-util/esnext\";\nimport xssFilter from \"xss\";\n\nexport { v4 as randomUUID } from \"uuid\";\n/**\n * Test if an object implements the DataView interface from vis-data.\n * Uses the idProp property instead of expecting a hardcoded id field \"id\".\n * @param {Object} obj The object to test.\n * @returns {boolean} True if the object implements vis-data DataView interface otherwise false.\n */\nexport function isDataViewLike(obj) {\n if (!obj) {\n return false;\n }\n let idProp = obj.idProp ?? obj._idProp;\n if (!idProp) {\n return false;\n }\n return isDataViewLikeUpstream(idProp, obj);\n}\n\n// parse ASP.Net Date pattern,\n// for example '/Date(1198908717056)/' or '/Date(1198908717056-0700)/'\n// code from http://momentjs.com/\nconst ASPDateRegex = /^\\/?Date\\((-?\\d+)/i;\nconst NumericRegex = /^\\d+$/;\n/**\n * Convert an object into another type\n *\n * @param {Object} object - Value of unknown type.\n * @param {string} type - Name of the desired type.\n *\n * @returns {Object} Object in the desired type.\n * @throws Error\n */\nexport function convert(object, type) {\n let match;\n\n if (object === undefined) {\n return undefined;\n }\n if (object === null) {\n return null;\n }\n\n if (!type) {\n return object;\n }\n if (!(typeof type === \"string\") && !(type instanceof String)) {\n throw new Error(\"Type must be a string\");\n }\n\n //noinspection FallthroughInSwitchStatementJS\n switch (type) {\n case \"boolean\":\n case \"Boolean\":\n return Boolean(object);\n\n case \"number\":\n case \"Number\":\n if (isString(object) && !isNaN(Date.parse(object))) {\n return moment(object).valueOf();\n } else {\n // @TODO: I don't think that Number and String constructors are a good idea.\n // This could also fail if the object doesn't have valueOf method or if it's redefined.\n // For example: Object.create(null) or { valueOf: 7 }.\n return Number(object.valueOf());\n }\n case \"string\":\n case \"String\":\n return String(object);\n\n case \"Date\":\n try {\n return convert(object, \"Moment\").toDate();\n } catch (e) {\n if (e instanceof TypeError) {\n throw new TypeError(\n \"Cannot convert object of type \" +\n getType(object) +\n \" to type \" +\n type,\n { cause: e },\n );\n } else {\n throw e;\n }\n }\n\n case \"Moment\":\n if (isNumber(object)) {\n return moment(object);\n }\n if (object instanceof Date) {\n return moment(object.valueOf());\n } else if (moment.isMoment(object)) {\n return moment(object);\n }\n if (isString(object)) {\n match = ASPDateRegex.exec(object);\n if (match) {\n // object is an ASP date\n return moment(Number(match[1])); // parse number\n }\n match = NumericRegex.exec(object);\n\n if (match) {\n return moment(Number(object));\n }\n\n return moment(object); // parse string\n } else {\n throw new TypeError(\n \"Cannot convert object of type \" +\n getType(object) +\n \" to type \" +\n type,\n );\n }\n\n case \"ISODate\":\n if (isNumber(object)) {\n return new Date(object);\n } else if (object instanceof Date) {\n return object.toISOString();\n } else if (moment.isMoment(object)) {\n return object.toDate().toISOString();\n } else if (isString(object)) {\n match = ASPDateRegex.exec(object);\n if (match) {\n // object is an ASP date\n return new Date(Number(match[1])).toISOString(); // parse number\n } else {\n return moment(object).format(); // ISO 8601\n }\n } else {\n throw new Error(\n \"Cannot convert object of type \" +\n getType(object) +\n \" to type ISODate\",\n );\n }\n\n case \"ASPDate\":\n if (isNumber(object)) {\n return \"/Date(\" + object + \")/\";\n } else if (object instanceof Date || moment.isMoment(object)) {\n return \"/Date(\" + object.valueOf() + \")/\";\n } else if (isString(object)) {\n match = ASPDateRegex.exec(object);\n let value;\n if (match) {\n // object is an ASP date\n value = new Date(Number(match[1])).valueOf(); // parse number\n } else {\n value = new Date(object).valueOf(); // parse string\n }\n return \"/Date(\" + value + \")/\";\n } else {\n throw new Error(\n \"Cannot convert object of type \" +\n getType(object) +\n \" to type ASPDate\",\n );\n }\n\n default:\n throw new Error(`Unknown type ${type}`);\n }\n}\n\n/**\n * Create a Data Set like wrapper to seamlessly coerce data types.\n *\n * @param {Object} rawDS - The Data Set with raw uncoerced data.\n * @param {Object} type - A record assigning a data type to property name.\n * @param {string} type.start - Data type name of property 'start'. Default: Date.\n * @param {string} type.end - Data type name of property 'end'. Default: Date.\n *\n * @remarks\n * The write operations (`add`, `remove`, `update` and `updateOnly`) write into\n * the raw (uncoerced) data set. These values are then picked up by a pipe\n * which coerces the values using the [[convert]] function and feeds them into\n * the coerced data set. When querying (`forEach`, `get`, `getIds`, `off` and\n * `on`) the values are then fetched from the coerced data set and already have\n * the required data types. The values are coerced only once when inserted and\n * then the same value is returned each time until it is updated or deleted.\n *\n * For example: `typeCoercedDataSet.add({ id: 7, start: \"2020-01-21\" })` would\n * result in `typeCoercedDataSet.get(7)` returning `{ id: 7, start: moment(new\n * Date(\"2020-01-21\")).toDate() }`.\n *\n * Use the dispose method prior to throwing a reference to this away. Otherwise\n * the pipe connecting the two Data Sets will keep the unaccessible coerced\n * Data Set alive and updated as long as the raw Data Set exists.\n *\n * @returns {Object} A Data Set like object that saves data into the raw Data Set and\n * retrieves them from the coerced Data Set.\n */\nexport function typeCoerceDataSet(\n rawDS,\n type = { start: \"Date\", end: \"Date\" },\n) {\n const idProp = rawDS._idProp;\n const coercedDS = new DataSet({ fieldId: idProp });\n\n const pipe = createNewDataPipeFrom(rawDS)\n .map((item) =>\n Object.keys(item).reduce((acc, key) => {\n acc[key] = convert(item[key], type[key]);\n return acc;\n }, {}),\n )\n .to(coercedDS);\n\n pipe.all().start();\n\n return {\n // Write only.\n add: (...args) => rawDS.getDataSet().add(...args),\n remove: (...args) => rawDS.getDataSet().remove(...args),\n update: (...args) => rawDS.getDataSet().update(...args),\n updateOnly: (...args) => rawDS.getDataSet().updateOnly(...args),\n clear: (...args) => rawDS.getDataSet().clear(...args),\n\n // Read only.\n forEach: coercedDS.forEach.bind(coercedDS),\n get: coercedDS.get.bind(coercedDS),\n getIds: coercedDS.getIds.bind(coercedDS),\n off: coercedDS.off.bind(coercedDS),\n on: coercedDS.on.bind(coercedDS),\n\n get length() {\n return coercedDS.length;\n },\n\n // Non standard.\n idProp,\n type,\n\n rawDS,\n coercedDS,\n dispose: () => pipe.stop(),\n };\n}\n\n// Configure XSS protection\nconst setupXSSCleaner = (options) => {\n const customXSS = new xssFilter.FilterXSS(options);\n\n return (input) => {\n if (typeof input === \"string\") {\n return customXSS.process(input);\n }\n return input; // Leave other types unchanged\n };\n};\nconst setupNoOpCleaner = (string) => string;\n\n// when nothing else is configured: filter XSS with the lib's default options\nlet configuredXSSProtection = setupXSSCleaner();\n\nconst setupXSSProtection = (options) => {\n // No options? Do nothing.\n if (!options) {\n return;\n }\n\n // Disable XSS protection completely on request\n if (options.disabled === true) {\n configuredXSSProtection = setupNoOpCleaner;\n console.warn(\n \"You disabled XSS protection for vis-Timeline. I sure hope you know what you're doing!\",\n );\n } else {\n // Configure XSS protection with some custom options.\n // For a list of valid options check the lib's documentation:\n // https://github.com/leizongmin/js-xss#custom-filter-rules\n if (options.filterOptions) {\n configuredXSSProtection = setupXSSCleaner(options.filterOptions);\n }\n }\n};\n\nconst availableUtils = {\n ...util,\n convert,\n setupXSSProtection,\n};\n\nObject.defineProperty(availableUtils, \"xss\", {\n get: function () {\n return configuredXSSProtection;\n },\n});\n\nexport default availableUtils;\n","// Utility functions for ordering and stacking of items\nconst EPSILON = 0.001; // used when checking collisions, to prevent round-off errors\n\n/**\n * Order items by their start data\n * @param {Item[]} items\n */\nexport function orderByStart(items) {\n items.sort((a, b) => a.data.start - b.data.start);\n}\n\n/**\n * Order items by their end date. If they have no end date, their start date\n * is used.\n * @param {Item[]} items\n */\nexport function orderByEnd(items) {\n items.sort((a, b) => {\n const aTime = \"end\" in a.data ? a.data.end : a.data.start;\n const bTime = \"end\" in b.data ? b.data.end : b.data.start;\n\n return aTime - bTime;\n });\n}\n\n/**\n * Adjust vertical positions of the items such that they don't overlap each\n * other.\n * @param {Item[]} items\n * All visible items\n * @param {{item: {horizontal: number, vertical: number}, axis: number}} margin\n * Margins between items and between items and the axis.\n * @param {boolean} [force=false]\n * If true, all items will be repositioned. If false (default), only\n * items having a top===null will be re-stacked\n * @param {function} shouldBailItemsRedrawFunction\n * bailing function\n * @return {boolean} shouldBail\n */\nexport function stack(items, margin, force, shouldBailItemsRedrawFunction) {\n const stackingResult = performStacking(\n items,\n margin.item,\n false,\n (item) => item.stack && (force || item.top === null),\n (item) => item.stack,\n () => margin.axis,\n shouldBailItemsRedrawFunction,\n );\n\n // If shouldBail function returned true during stacking calculation\n return stackingResult === null;\n}\n\n/**\n * Adjust vertical positions of the items within a single subgroup such that they\n * don't overlap each other.\n * @param {Item[]} items\n * All items withina subgroup\n * @param {{item: {horizontal: number, vertical: number}, axis: number}} margin\n * Margins between items and between items and the axis.\n * @param {subgroup} subgroup\n * The subgroup that is being stacked\n */\nexport function substack(items, margin, subgroup) {\n const subgroupHeight = performStacking(\n items,\n margin.item,\n false,\n (item) => item.stack,\n () => true,\n (item) => item.baseTop,\n );\n subgroup.height = subgroupHeight - subgroup.top + 0.5 * margin.item.vertical;\n}\n\n/**\n * Adjust vertical positions of the items without stacking them\n * @param {Item[]} items\n * All visible items\n * @param {{item: {horizontal: number, vertical: number}, axis: number}} margin\n * Margins between items and between items and the axis.\n * @param {subgroups[]} subgroups\n * All subgroups\n * @param {boolean} isStackSubgroups\n */\nexport function nostack(items, margin, subgroups, isStackSubgroups) {\n for (let i = 0; i < items.length; i++) {\n if (items[i].data.subgroup == undefined) {\n items[i].top = margin.item.vertical;\n continue;\n }\n if (items[i].data.subgroup === undefined || !isStackSubgroups) continue;\n\n let newTop = 0;\n for (const subgroup in subgroups) {\n if (\n !Object.prototype.hasOwnProperty.call(subgroups, subgroup) ||\n subgroups[subgroup].visible !== true ||\n subgroups[subgroup].index >= subgroups[items[i].data.subgroup].index\n )\n continue;\n\n newTop += subgroups[subgroup].height;\n subgroups[items[i].data.subgroup].top = newTop;\n }\n items[i].top = newTop + 0.5 * margin.item.vertical;\n }\n\n if (!isStackSubgroups) stackSubgroups(items, margin, subgroups);\n}\n\n/**\n * Adjust vertical positions of the subgroups such that they don't overlap each\n * other.\n * @param {Array.<timeline.Item>} items\n * @param {{item: {horizontal: number, vertical: number}, axis: number}} margin Margins between items and between items and the axis.\n * @param {subgroups[]} subgroups\n * All subgroups\n */\nexport function stackSubgroups(items, margin, subgroups) {\n performStacking(\n Object.values(subgroups).toSorted((a, b) => {\n if (a.index > b.index) return 1;\n if (a.index < b.index) return -1;\n return 0;\n }),\n {\n vertical: 0,\n },\n true,\n () => true,\n () => true,\n () => 0,\n );\n\n for (let i = 0; i < items.length; i++) {\n if (items[i].data.subgroup !== undefined) {\n items[i].top =\n subgroups[items[i].data.subgroup].top + 0.5 * margin.item.vertical;\n }\n }\n}\n\n/**\n * Adjust vertical positions of the subgroups such that they don't overlap each\n * other, then stacks the contents of each subgroup individually.\n * @param {Item[]} subgroupItems\n * All the items in a subgroup\n * @param {{item: {horizontal: number, vertical: number}, axis: number}} margin\n * Margins between items and between items and the axis.\n * @param {subgroups[]} subgroups\n * All subgroups\n */\nexport function stackSubgroupsWithInnerStack(subgroupItems, margin, subgroups) {\n let doSubStack = false;\n\n // Run subgroups in their order (if any)\n const subgroupOrder = [];\n\n for (let subgroup in subgroups) {\n if (Object.prototype.hasOwnProperty.call(subgroups[subgroup], \"index\")) {\n subgroupOrder[subgroups[subgroup].index] = subgroup;\n } else {\n subgroupOrder.push(subgroup);\n }\n }\n\n for (let j = 0; j < subgroupOrder.length; j++) {\n let subgroup = subgroupOrder[j];\n if (!Object.prototype.hasOwnProperty.call(subgroups, subgroup)) continue;\n\n doSubStack = doSubStack || subgroups[subgroup].stack;\n subgroups[subgroup].top = 0;\n\n for (const otherSubgroup in subgroups) {\n if (\n subgroups[otherSubgroup].visible &&\n subgroups[subgroup].index > subgroups[otherSubgroup].index\n ) {\n subgroups[subgroup].top += subgroups[otherSubgroup].height;\n }\n }\n\n const items = subgroupItems[subgroup];\n for (let i = 0; i < items.length; i++) {\n if (items[i].data.subgroup === undefined) continue;\n\n items[i].top =\n subgroups[items[i].data.subgroup].top + 0.5 * margin.item.vertical;\n if (subgroups[subgroup].stack) items[i].baseTop = items[i].top;\n }\n\n if (doSubStack && subgroups[subgroup].stack)\n substack(subgroupItems[subgroup], margin, subgroups[subgroup]);\n }\n}\n\n/**\n * Reusable stacking function\n *\n * @param {Item[]} items\n * An array of items to consider during stacking.\n * @param {{horizontal: number, vertical: number}} margins\n * Margins to be used for collision checking and placement of items.\n * @param {boolean} compareTimes\n * By default, horizontal collision is checked based on the spatial position of the items (left/right and width).\n * If this argument is true, horizontal collision will instead be checked based on the start/end times of each item.\n * Vertical collision is always checked spatially.\n * @param {function(Item): number | null} shouldStack\n * A callback function which is called before we start to process an item. The return value indicates whether the item will be processed.\n * @param {function(Item): boolean} shouldOthersStack\n * A callback function which indicates whether other items should consider this item when being stacked.\n * @param {function(Item): number} getInitialHeight\n * A callback function which determines the height items are initially placed at\n * @param {function(): boolean} shouldBail\n * A callback function which should indicate if the stacking process should be aborted.\n *\n * @returns {null|number}\n * if shouldBail was triggered, returns null\n * otherwise, returns the maximum height\n */\nfunction performStacking(\n items,\n margins,\n compareTimes,\n shouldStack,\n shouldOthersStack,\n getInitialHeight,\n shouldBail,\n) {\n // Time-based horizontal comparison\n let getItemStart = (item) => item.start;\n let getItemEnd = (item) => item.end;\n if (!compareTimes) {\n // Spatial horizontal comparisons\n const rtl = !!(items[0] && items[0].options.rtl);\n if (rtl) {\n getItemStart = (item) => item.right;\n } else {\n getItemStart = (item) => item.left;\n }\n getItemEnd = (item) => getItemStart(item) + item.width + margins.horizontal;\n }\n\n const itemsToPosition = [];\n const itemsAlreadyPositioned = []; // It's vital that this array is kept sorted based on the start of each item\n\n // If the order we needed to place items was based purely on the start of each item, we could calculate stacking very efficiently.\n // Unfortunately for us, this is not guaranteed. But the order is often based on the start of items at least to some degree, and\n // we can use this to make some optimisations. While items are proceeding in order of start, we can keep moving our search indexes\n // forwards. Then if we encounter an item that's out of order, we reset our indexes and search from the beginning of the array again.\n let previousStart = null;\n let insertionIndex = 0;\n\n // First let's handle any immoveable items\n for (const item of items) {\n if (shouldStack(item)) {\n itemsToPosition.push(item);\n } else {\n if (shouldOthersStack(item)) {\n const itemStart = getItemStart(item);\n\n // We need to put immoveable items into itemsAlreadyPositioned and ensure that this array is sorted.\n // We could simply insert them, and then use JavaScript's sort function to sort them afterwards.\n // This would achieve an average complexity of O(n log n).\n //\n // Instead, I'm gambling that the start of each item will usually be the same or later than the\n // start of the previous item. While this holds (best case), we can insert items in O(n).\n // In the worst case (where each item starts before the previous item) this grows to O(n^2).\n //\n // I am making the assumption that for most datasets, the \"order\" function will have relatively low cardinality,\n // and therefore this tradeoff should be easily worth it.\n if (previousStart !== null && itemStart < previousStart - EPSILON) {\n insertionIndex = 0;\n }\n previousStart = itemStart;\n\n insertionIndex = findIndexFrom(\n itemsAlreadyPositioned,\n (i) => getItemStart(i) - EPSILON > itemStart,\n insertionIndex,\n );\n\n itemsAlreadyPositioned.splice(insertionIndex, 0, item);\n insertionIndex++;\n }\n }\n }\n\n // Now we can loop through each item (in order) and find a position for them\n previousStart = null;\n let previousEnd = null;\n insertionIndex = 0;\n let horizontalOverlapStartIndex = 0;\n let horizontalOverlapEndIndex = 0;\n let maxHeight = 0;\n while (itemsToPosition.length > 0) {\n const item = itemsToPosition.shift();\n\n item.top = getInitialHeight(item);\n\n const itemStart = getItemStart(item);\n const itemEnd = getItemEnd(item);\n if (previousStart !== null && itemStart < previousStart - EPSILON) {\n horizontalOverlapStartIndex = 0;\n horizontalOverlapEndIndex = 0;\n insertionIndex = 0;\n previousEnd = null;\n }\n if (previousStart === null || itemStart > previousStart + EPSILON) {\n // Take advantage of the sorted itemsAlreadyPositioned array to narrow down the search\n horizontalOverlapStartIndex = findIndexFrom(\n itemsAlreadyPositioned,\n (i) => itemStart < getItemEnd(i) - EPSILON,\n horizontalOverlapStartIndex,\n );\n }\n previousStart = itemStart;\n\n // Since items aren't sorted by end time, it might increase or decrease from one item to the next. In order to keep an efficient search area, we will seek forwards/backwards accordingly.\n if (previousEnd === null || previousEnd < itemEnd - EPSILON) {\n horizontalOverlapEndIndex = findIndexFrom(\n itemsAlreadyPositioned,\n (i) => itemEnd < getItemStart(i) - EPSILON,\n Math.max(horizontalOverlapStartIndex, horizontalOverlapEndIndex),\n );\n }\n if (previousEnd !== null && previousEnd - EPSILON > itemEnd) {\n horizontalOverlapEndIndex =\n findLastIndexBetween(\n itemsAlreadyPositioned,\n (i) => itemEnd + EPSILON >= getItemStart(i),\n horizontalOverlapStartIndex,\n horizontalOverlapEndIndex,\n ) + 1;\n }\n previousEnd = itemEnd;\n\n // Sort by vertical position so we don't have to reconsider past items if we move an item\n const horizontallyCollidingItems = filterBetween(\n itemsAlreadyPositioned,\n (i) => itemStart < getItemEnd(i) - EPSILON,\n horizontalOverlapStartIndex,\n horizontalOverlapEndIndex,\n ).toSorted((a, b) => a.top - b.top);\n\n // Keep moving the item down until it stops colliding with any other items\n for (let i2 = 0; i2 < horizontallyCollidingItems.length; i2++) {\n const otherItem = horizontallyCollidingItems[i2];\n\n if (checkVerticalSpatialCollision(item, otherItem, margins)) {\n item.top = otherItem.top + otherItem.height + margins.vertical;\n }\n }\n\n if (shouldOthersStack(item)) {\n // Insert the item into itemsAlreadyPositioned, ensuring itemsAlreadyPositioned remains sorted.\n // In the best case, we can insert an item in constant time O(1). In the worst case, we insert an item in linear time O(n).\n // In both cases, this is better than doing a naive insert and then sort, which would cost on average O(n log n).\n insertionIndex = findIndexFrom(\n itemsAlreadyPositioned,\n (i) => getItemStart(i) - EPSILON > itemStart,\n insertionIndex,\n );\n\n itemsAlreadyPositioned.splice(insertionIndex, 0, item);\n\n if (insertionIndex < horizontalOverlapStartIndex) {\n horizontalOverlapStartIndex++;\n }\n\n if (insertionIndex <= horizontalOverlapEndIndex) {\n horizontalOverlapEndIndex++;\n }\n\n insertionIndex++;\n }\n\n // Keep track of the tallest item we've seen before\n const currentHeight = item.top + item.height;\n if (currentHeight > maxHeight) {\n maxHeight = currentHeight;\n }\n\n if (shouldBail && shouldBail()) {\n return null;\n }\n }\n\n return maxHeight;\n}\n\n/**\n * Test if the two provided items collide\n * The items must have parameters left, width, top, and height.\n * @param {Item} a The first item\n * @param {Item} b The second item\n * @param {{vertical: number}} margin\n * An object containing a horizontal and vertical\n * minimum required margin.\n * @return {boolean} true if a and b collide, else false\n */\nfunction checkVerticalSpatialCollision(a, b, margin) {\n return (\n a.top - margin.vertical + EPSILON < b.top + b.height &&\n a.top + a.height + margin.vertical - EPSILON > b.top\n );\n}\n\n/**\n * Find index of first item to meet predicate after a certain index.\n * If no such item is found, returns the length of the array.\n *\n * @param {any[]} arr The array\n * @param {function(item): boolean} predicate A function that should return true when a suitable item is found\n * @param {number|undefined} startIndex The index to start search from (inclusive). Optional, if not provided will search from the beginning of the array.\n *\n * @return {number}\n */\nfunction findIndexFrom(arr, predicate, startIndex) {\n if (!startIndex) {\n startIndex = 0;\n }\n for (let i = startIndex; i < arr.length; i++) {\n if (predicate(arr[i])) {\n return i;\n }\n }\n return arr.length;\n}\n\n/**\n * Find index of last item to meet predicate within a given range.\n * If no such item is found, returns the index prior to the start of the range.\n *\n * @param {any[]} arr The array\n * @param {function(item): boolean} predicate A function that should return true when a suitable item is found\n * @param {number|undefined} startIndex The earliest index to search to (inclusive). Optional, if not provided will continue until the start of the array.\n * @param {number|undefined} endIndex The end of the search range (exclusive). The search will begin on the index prior to this value. Optional, defaults to the end of array.\n *\n * @return {number}\n */\nfunction findLastIndexBetween(arr, predicate, startIndex, endIndex) {\n if (!startIndex) {\n startIndex = 0;\n }\n\n if (!endIndex) {\n endIndex = arr.length;\n }\n\n for (let i = endIndex - 1; i >= startIndex; i--) {\n if (predicate(arr[i])) {\n return i;\n }\n }\n\n return startIndex - 1;\n}\n\n/**\n * Takes an array and returns an array containing only items which meet a predicate within a given range.\n *\n * @param {any[]} arr The array\n * @param {function(item): boolean} predicate A function that should return true for items which should be included within the result\n * @param {number|undefined} startIndex The earliest index to include (inclusive). Optional, if not provided will continue until the start of the array.\n * @param {number|undefined} endIndex The end of the range to filter (exclusive). Optional, defaults to the end of array.\n *\n * @return {number}\n */\nfunction filterBetween(arr, predicate, startIndex, endIndex) {\n if (!startIndex) {\n startIndex = 0;\n }\n if (endIndex) {\n endIndex = Math.min(endIndex, arr.length);\n } else {\n endIndex = arr.length;\n }\n\n const result = [];\n for (let i = startIndex; i < endIndex; i++) {\n if (predicate(arr[i])) {\n result.push(arr[i]);\n }\n }\n return result;\n}\n","import util from \"../../util.js\";\nimport * as stack from \"../Stack.js\";\n\nconst UNGROUPED = \"__ungrouped__\"; // reserved group id for ungrouped items\nconst BACKGROUND = \"__background__\"; // reserved group id for background items without group\n\nexport const ReservedGroupIds = {\n UNGROUPED,\n BACKGROUND,\n};\n\n/**\n * @constructor Group\n */\nclass Group {\n /**\n * @param {number | string} groupId\n * @param {Object} data\n * @param {ItemSet} itemSet\n * @constructor Group\n */\n constructor(groupId, data, itemSet) {\n this.groupId = groupId;\n this.subgroups = {};\n this.subgroupStack = {};\n this.subgroupStackAll = false;\n this.subgroupVisibility = {};\n this.doInnerStack = false;\n this.shouldBailStackItems = false;\n this.subgroupIndex = 0;\n this.subgroupOrderer = data && data.subgroupOrder;\n this.itemSet = itemSet;\n this.isVisible = null;\n this.height = 0;\n this.stackDirty = true; // if true, items will be restacked on next redraw\n\n // This is a stack of functions (`() => void`) that will be executed before\n // the instance is disposed off (method `dispose`). Anything that needs to\n // be manually disposed off before garbage collection happens (or so that\n // garbage collection can happen) should be added to this stack.\n this._disposeCallbacks = [];\n\n if (data && data.nestedGroups) {\n this.nestedGroups = data.nestedGroups;\n if (data.showNested == false) {\n this.showNested = false;\n } else {\n this.showNested = true;\n }\n }\n\n if (data && data.subgroupStack) {\n if (typeof data.subgroupStack === \"boolean\") {\n this.doInnerStack = data.subgroupStack;\n this.subgroupStackAll = data.subgroupStack;\n } else {\n // We might be doing stacking on specific sub groups, but only\n // if at least one is set to do stacking\n for (const key in data.subgroupStack) {\n if (!Object.prototype.hasOwnProperty.call(data.subgroupStack, key))\n continue;\n this.subgroupStack[key] = data.subgroupStack[key];\n this.doInnerStack = this.doInnerStack || data.subgroupStack[key];\n }\n }\n }\n\n if (data && data.heightMode) {\n this.heightMode = data.heightMode;\n } else {\n this.heightMode = itemSet.options.groupHeightMode;\n }\n\n this.nestedInGroup = null;\n\n this.dom = {};\n this.props = {\n label: {\n width: 0,\n height: 0,\n },\n };\n this.className = null;\n\n this.items = {}; // items filtered by groupId of this group\n this.visibleItems = []; // items currently visible in window\n this.itemsInRange = []; // items currently in range\n this.orderedItems = {\n byStart: [],\n byEnd: [],\n };\n this.checkRangedItems = false; // needed to refresh the ranged items if the window is programatically changed with NO overlap.\n\n const handleCheckRangedItems = () => {\n this.checkRangedItems = true;\n };\n this.itemSet.body.emitter.on(\"checkRangedItems\", handleCheckRangedItems);\n this._disposeCallbacks.push(() => {\n this.itemSet.body.emitter.off(\"checkRangedItems\", handleCheckRangedItems);\n });\n\n this._create();\n\n this.setData(data);\n }\n\n /**\n * Create DOM elements for the group\n * @private\n */\n _create() {\n const label = document.createElement(\"div\");\n if (this.itemSet.options.groupEditable.order) {\n label.className = \"vis-label draggable\";\n } else {\n label.className = \"vis-label\";\n }\n this.dom.label = label;\n\n const inner = document.createElement(\"div\");\n inner.className = \"vis-inner\";\n label.appendChild(inner);\n this.dom.inner = inner;\n\n const foreground = document.createElement(\"div\");\n foreground.className = \"vis-group\";\n foreground[\"vis-group\"] = this;\n this.dom.foreground = foreground;\n\n this.dom.background = document.createElement(\"div\");\n this.dom.background.className = \"vis-group\";\n\n this.dom.axis = document.createElement(\"div\");\n this.dom.axis.className = \"vis-group\";\n\n // create a hidden marker to detect when the Timelines container is attached\n // to the DOM, or the style of a parent of the Timeline is changed from\n // display:none is changed to visible.\n this.dom.marker = document.createElement(\"div\");\n this.dom.marker.style.visibility = \"hidden\";\n this.dom.marker.style.position = \"absolute\";\n this.dom.marker.innerHTML = \"\";\n this.dom.background.appendChild(this.dom.marker);\n }\n\n /**\n * Set the group data for this group\n * @param {Object} data Group data, can contain properties content and className\n */\n setData(data) {\n if (this.itemSet.groupTouchParams.isDragging) return;\n\n // update contents\n let content;\n let templateFunction;\n\n if (data && data.subgroupVisibility) {\n for (const key in data.subgroupVisibility) {\n if (!Object.prototype.hasOwnProperty.call(data.subgroupVisibility, key))\n continue;\n this.subgroupVisibility[key] = data.subgroupVisibility[key];\n }\n }\n\n if (this.itemSet.options && this.itemSet.options.groupTemplate) {\n templateFunction = this.itemSet.options.groupTemplate.bind(this);\n content = templateFunction(data, this.dom.inner);\n } else {\n content = data && data.content;\n }\n\n if (content instanceof Element) {\n while (this.dom.inner.firstChild) {\n this.dom.inner.removeChild(this.dom.inner.firstChild);\n }\n this.dom.inner.appendChild(content);\n } else if (content instanceof Object && content.isReactComponent) {\n // Do nothing. Component was rendered into the node be ReactDOM.render.\n // That branch is necessary for evasion of a second call templateFunction.\n // Supports only React < 16(due to the asynchronous nature of React 16).\n } else if (content instanceof Object) {\n templateFunction(data, this.dom.inner);\n } else if (content !== undefined && content !== null) {\n this.dom.inner.innerHTML = util.xss(content);\n } else {\n this.dom.inner.innerHTML = util.xss(this.groupId || \"\"); // groupId can be null\n }\n\n // update title\n this.dom.label.title = (data && data.title) || \"\";\n if (!this.dom.inner.firstChild) {\n util.addClassName(this.dom.inner, \"vis-hidden\");\n } else {\n util.removeClassName(this.dom.inner, \"vis-hidden\");\n }\n\n if (data && data.nestedGroups) {\n if (!this.nestedGroups || this.nestedGroups != data.nestedGroups) {\n this.nestedGroups = data.nestedGroups;\n }\n\n if (data.showNested !== undefined || this.showNested === undefined) {\n if (data.showNested == false) {\n this.showNested = false;\n } else {\n this.showNested = true;\n }\n }\n\n util.addClassName(this.dom.label, \"vis-nesting-group\");\n if (this.showNested) {\n util.removeClassName(this.dom.label, \"collapsed\");\n util.addClassName(this.dom.label, \"expanded\");\n } else {\n util.removeClassName(this.dom.label, \"expanded\");\n util.addClassName(this.dom.label, \"collapsed\");\n }\n } else if (this.nestedGroups) {\n this.nestedGroups = null;\n util.removeClassName(this.dom.label, \"collapsed\");\n util.removeClassName(this.dom.label, \"expanded\");\n util.removeClassName(this.dom.label, \"vis-nesting-group\");\n }\n\n if (data && (data.treeLevel || data.nestedInGroup)) {\n util.addClassName(this.dom.label, \"vis-nested-group\");\n if (data.treeLevel) {\n util.addClassName(this.dom.label, \"vis-group-level-\" + data.treeLevel);\n } else {\n // Nesting level is unknown, but we're sure it's at least 1\n util.addClassName(this.dom.label, \"vis-group-level-unknown-but-gte1\");\n }\n } else {\n util.addClassName(this.dom.label, \"vis-group-level-0\");\n }\n\n // update className\n const className = (data && data.className) || null;\n if (className != this.className) {\n if (this.className) {\n util.removeClassName(this.dom.label, this.className);\n util.removeClassName(this.dom.foreground, this.className);\n util.removeClassName(this.dom.background, this.className);\n util.removeClassName(this.dom.axis, this.className);\n }\n util.addClassName(this.dom.label, className);\n util.addClassName(this.dom.foreground, className);\n util.addClassName(this.dom.background, className);\n util.addClassName(this.dom.axis, className);\n this.className = className;\n }\n\n // update style\n if (this.style) {\n util.removeCssText(this.dom.label, this.style);\n this.style = null;\n }\n if (data && data.style) {\n util.addCssText(this.dom.label, data.style);\n this.style = data.style;\n }\n }\n\n /**\n * Get the width of the group label\n * @return {number} width\n */\n getLabelWidth() {\n return this.props.label.width;\n }\n\n /**\n * check if group has had an initial height hange\n * @returns {boolean}\n */\n _didMarkerHeightChange() {\n const markerHeight = this.dom.marker.clientHeight;\n if (markerHeight != this.lastMarkerHeight) {\n this.lastMarkerHeight = markerHeight;\n const redrawQueue = {};\n let redrawQueueLength = 0;\n\n util.forEach(this.items, (item, key) => {\n item.dirty = true;\n if (item.displayed) {\n const returnQueue = true;\n redrawQueue[key] = item.redraw(returnQueue);\n redrawQueueLength = redrawQueue[key].length;\n }\n });\n\n const needRedraw = redrawQueueLength > 0;\n if (needRedraw) {\n // redraw all regular items\n for (let i = 0; i < redrawQueueLength; i++) {\n util.forEach(redrawQueue, (fns) => {\n fns[i]();\n });\n }\n }\n return true;\n } else {\n return false;\n }\n }\n\n /**\n * calculate group dimentions and position\n * @param {number} pixels\n */\n _calculateGroupSizeAndPosition() {\n const { offsetTop, offsetLeft, offsetWidth } = this.dom.foreground;\n this.top = offsetTop;\n this.right = offsetLeft;\n this.width = offsetWidth;\n }\n\n /**\n * checks if should bail redraw of items\n * @returns {boolean} should bail\n */\n _shouldBailItemsRedraw() {\n const me = this;\n const timeoutOptions = this.itemSet.options.onTimeout;\n const bailOptions = {\n relativeBailingTime: this.itemSet.itemsSettingTime,\n bailTimeMs: timeoutOptions && timeoutOptions.timeoutMs,\n userBailFunction: timeoutOptions && timeoutOptions.callback,\n shouldBailStackItems: this.shouldBailStackItems,\n };\n let bail = null;\n if (!this.itemSet.initialDrawDone) {\n if (bailOptions.shouldBailStackItems) {\n return true;\n }\n if (\n Math.abs(Date.now() - new Date(bailOptions.relativeBailingTime)) >\n bailOptions.bailTimeMs\n ) {\n if (\n bailOptions.userBailFunction &&\n this.itemSet.userContinueNotBail == null\n ) {\n bailOptions.userBailFunction((didUserContinue) => {\n me.itemSet.userContinueNotBail = didUserContinue;\n bail = !didUserContinue;\n });\n } else if (me.itemSet.userContinueNotBail == false) {\n bail = true;\n } else {\n bail = false;\n }\n }\n }\n\n return bail;\n }\n\n /**\n * redraws items\n * @param {boolean} forceRestack\n * @param {boolean} lastIsVisible\n * @param {number} margin\n * @param {object} range\n * @private\n */\n _redrawItems(forceRestack, lastIsVisible, margin, range) {\n const restack =\n forceRestack || this.stackDirty || (this.isVisible && !lastIsVisible);\n\n // if restacking, reposition visible items vertically\n if (restack) {\n const orderedItems = {\n byEnd: this.orderedItems.byEnd.filter((item) => !item.isCluster),\n byStart: this.orderedItems.byStart.filter((item) => !item.isCluster),\n };\n\n const orderedClusters = {\n byEnd: [\n ...new Set(\n this.orderedItems.byEnd\n .map((item) => item.cluster)\n .filter((item) => !!item),\n ),\n ],\n byStart: [\n ...new Set(\n this.orderedItems.byStart\n .map((item) => item.cluster)\n .filter((item) => !!item),\n ),\n ],\n };\n\n /**\n * Get all visible items in range\n * @return {array} item