} path
* @param {{}} optionsObj
* @returns {{}}
* @private
*/
_constructOptions(value, path, optionsObj = {}) {
let pointer = optionsObj;
// when dropdown boxes can be string or boolean, we typecast it into correct types
value = value === 'true' ? true : value;
value = value === 'false' ? false : value;
for (let i = 0; i < path.length; i++) {
if (path[i] !== 'global') {
if (pointer[path[i]] === undefined) {
pointer[path[i]] = {};
}
if (i !== path.length - 1) {
pointer = pointer[path[i]];
}
else {
pointer[path[i]] = value;
}
}
}
return optionsObj;
}
/**
* @private
*/
_printOptions() {
let options = this.getOptions();
this.optionsContainer.innerHTML = 'var options = ' + JSON.stringify(options, null, 2) + '
';
}
/**
*
* @returns {{}} options
*/
getOptions() {
let options = {};
for (var i = 0; i < this.changedOptions.length; i++) {
this._constructOptions(this.changedOptions[i].value, this.changedOptions[i].path, options);
}
return options;
}
}
/**
* Create a timeline visualization
* @extends Core
*/
class Timeline extends Core {
/**
* @param {HTMLElement} container
* @param {vis.DataSet | vis.DataView | Array} [items]
* @param {vis.DataSet | vis.DataView | Array} [groups]
* @param {Object} [options] See Timeline.setOptions for the available options.
* @constructor Timeline
*/
constructor(container, items, groups, options) {
super();
this.initTime = new Date();
this.itemsDone = false;
if (!(this instanceof Timeline)) {
throw new SyntaxError('Constructor must be called with the new operator');
}
// if the third element is options, the forth is groups (optionally);
if (!(Array.isArray(groups) || isDataViewLike("id", groups)) && groups instanceof Object) {
const forthArgument = options;
options = groups;
groups = forthArgument;
}
// TODO: REMOVE THIS in the next MAJOR release
// see https://github.com/almende/vis/issues/2511
if (options && options.throttleRedraw) {
console.warn("Timeline option \"throttleRedraw\" is DEPRICATED and no longer supported. It will be removed in the next MAJOR release.");
}
const me = this;
this.defaultOptions = {
autoResize: true,
longSelectPressTime: 251,
orientation: {
axis: 'bottom', // axis orientation: 'bottom', 'top', or 'both'
item: 'bottom' // not relevant
},
moment: moment$1,
};
this.options = util$3.deepExtend({}, this.defaultOptions);
// Create the DOM, props, and emitter
this._create(container);
if (!options || (options && typeof options.rtl == "undefined")) {
this.dom.root.style.visibility = 'hidden';
let directionFromDom;
let domNode = this.dom.root;
while (!directionFromDom && domNode) {
directionFromDom = window.getComputedStyle(domNode, null).direction;
domNode = domNode.parentElement;
}
this.options.rtl = (directionFromDom && (directionFromDom.toLowerCase() == "rtl"));
} else {
this.options.rtl = options.rtl;
}
if (options) {
if (options.rollingMode) { this.options.rollingMode = options.rollingMode; }
if (options.onInitialDrawComplete) { this.options.onInitialDrawComplete = options.onInitialDrawComplete; }
if (options.onTimeout) { this.options.onTimeout = options.onTimeout; }
if (options.loadingScreenTemplate) { this.options.loadingScreenTemplate = options.loadingScreenTemplate; }
}
// Prepare loading screen
const loadingScreenFragment = document.createElement('div');
if (this.options.loadingScreenTemplate) {
const templateFunction = this.options.loadingScreenTemplate.bind(this);
const loadingScreen = templateFunction(this.dom.loadingScreen);
if ((loadingScreen instanceof Object) && !(loadingScreen instanceof Element)) {
templateFunction(loadingScreenFragment);
} else {
if (loadingScreen instanceof Element) {
loadingScreenFragment.innerHTML = '';
loadingScreenFragment.appendChild(loadingScreen);
}
else if (loadingScreen != undefined) {
loadingScreenFragment.innerHTML = util$3.xss(loadingScreen);
}
}
}
this.dom.loadingScreen.appendChild(loadingScreenFragment);
// all components listed here will be repainted automatically
this.components = [];
this.body = {
dom: this.dom,
domProps: this.props,
emitter: {
on: this.on.bind(this),
off: this.off.bind(this),
emit: this.emit.bind(this)
},
hiddenDates: [],
util: {
getScale() {
return me.timeAxis.step.scale;
},
getStep() {
return me.timeAxis.step.step;
},
toScreen: me._toScreen.bind(me),
toGlobalScreen: me._toGlobalScreen.bind(me), // this refers to the root.width
toTime: me._toTime.bind(me),
toGlobalTime : me._toGlobalTime.bind(me)
}
};
// range
this.range = new Range(this.body, this.options);
this.components.push(this.range);
this.body.range = this.range;
// time axis
this.timeAxis = new TimeAxis(this.body, this.options);
this.timeAxis2 = null; // used in case of orientation option 'both'
this.components.push(this.timeAxis);
// current time bar
this.currentTime = new CurrentTime(this.body, this.options);
this.components.push(this.currentTime);
// item set
this.itemSet = new ItemSet(this.body, this.options);
this.components.push(this.itemSet);
this.itemsData = null; // DataSet
this.groupsData = null; // DataSet
function emit(eventName, event) {
if (!me.hasListeners(eventName)) {
return;
}
me.emit(eventName, me.getEventProperties(event));
}
this.dom.root.onclick = event => {
emit('click', event);
};
this.dom.root.ondblclick = event => {
emit('doubleClick', event);
};
this.dom.root.oncontextmenu = event => {
emit('contextmenu', event);
};
this.dom.root.onmouseover = event => {
emit('mouseOver', event);
};
if(window.PointerEvent) {
this.dom.root.onpointerdown = event => {
emit('mouseDown', event);
};
this.dom.root.onpointermove = event => {
emit('mouseMove', event);
};
this.dom.root.onpointerup = event => {
emit('mouseUp', event);
};
} else {
this.dom.root.onmousemove = event => {
emit('mouseMove', event);
};
this.dom.root.onmousedown = event => {
emit('mouseDown', event);
};
this.dom.root.onmouseup = event => {
emit('mouseUp', event);
};
}
//Single time autoscale/fit
this.initialFitDone = false;
this.on('changed', () => {
if (me.itemsData == null) return;
if (!me.initialFitDone && !me.options.rollingMode) {
me.initialFitDone = true;
if (me.options.start != undefined || me.options.end != undefined) {
if (me.options.start == undefined || me.options.end == undefined) {
var range = me.getItemRange();
}
const start = me.options.start != undefined ? me.options.start : range.min;
const end = me.options.end != undefined ? me.options.end : range.max;
me.setWindow(start, end, {animation: false});
} else {
me.fit({animation: false});
}
}
if (!me.initialDrawDone && (me.initialRangeChangeDone || (!me.options.start && !me.options.end)
|| me.options.rollingMode)) {
me.initialDrawDone = true;
me.itemSet.initialDrawDone = true;
me.dom.root.style.visibility = 'visible';
me.dom.loadingScreen.parentNode.removeChild(me.dom.loadingScreen);
if (me.options.onInitialDrawComplete) {
setTimeout(() => {
return me.options.onInitialDrawComplete();
}, 0);
}
}
});
this.on('destroyTimeline', () => {
me.destroy();
});
// apply options
if (options) {
this.setOptions(options);
}
this.body.emitter.on('fit', (args) => {
this._onFit(args);
this.redraw();
});
// IMPORTANT: THIS HAPPENS BEFORE SET ITEMS!
if (groups) {
this.setGroups(groups);
}
// create itemset
if (items) {
this.setItems(items);
}
// draw for the first time
this._redraw();
}
/**
* Load a configurator
* @return {Object}
* @private
*/
_createConfigurator() {
return new Configurator$2(this, this.dom.container, configureOptions);
}
/**
* Force a redraw. The size of all items will be recalculated.
* Can be useful to manually redraw when option autoResize=false and the window
* has been resized, or when the items CSS has been changed.
*
* Note: this function will be overridden on construction with a trottled version
*/
redraw() {
this.itemSet && this.itemSet.markDirty({refreshItems: true});
this._redraw();
}
/**
* Remove an item from the group
* @param {object} options
*/
setOptions(options) {
// validate options
let errorFound = Validator$2.validate(options, allOptions$1$1);
if (errorFound === true) {
console.log('%cErrors have been found in the supplied options object.', printStyle);
}
Core.prototype.setOptions.call(this, options);
if ('type' in options) {
if (options.type !== this.options.type) {
this.options.type = options.type;
// force recreation of all items
const itemsData = this.itemsData;
if (itemsData) {
const selection = this.getSelection();
this.setItems(null); // remove all
this.setItems(itemsData.rawDS); // add all
this.setSelection(selection); // restore selection
}
}
}
}
/**
* Set items
* @param {vis.DataSet | Array | null} items
*/
setItems(items) {
this.itemsDone = false;
// convert to type DataSet when needed
let newDataSet;
if (!items) {
newDataSet = null;
}
else if (isDataViewLike("id", items)) {
newDataSet = typeCoerceDataSet(items);
}
else {
// turn an array into a dataset
newDataSet = typeCoerceDataSet(new DataSet(items));
}
// set items
if (this.itemsData) {
// stop maintaining a coerced version of the old data set
this.itemsData.dispose();
}
this.itemsData = newDataSet;
this.itemSet && this.itemSet.setItems(newDataSet != null ? newDataSet.rawDS : null);
}
/**
* Set groups
* @param {vis.DataSet | Array} groups
*/
setGroups(groups) {
// convert to type DataSet when needed
let newDataSet;
const filter = group => group.visible !== false;
if (!groups) {
newDataSet = null;
}
else {
// If groups is array, turn to DataSet & build dataview from that
if (Array.isArray(groups)) groups = new DataSet(groups);
newDataSet = new DataView(groups,{filter});
}
// This looks weird but it's necessary to prevent memory leaks.
//
// The problem is that the DataView will exist as long as the DataSet it's
// connected to. This will force it to swap the groups DataSet for it's own
// DataSet. In this arrangement it will become unreferenced from the outside
// and garbage collected.
//
// IMPORTANT NOTE: If `this.groupsData` is a DataView was created in this
// method. Even if the original is a DataView already a new one has been
// created and assigned to `this.groupsData`. In case this changes in the
// future it will be necessary to rework this!!!!
if (this.groupsData != null && typeof this.groupsData.setData === "function") {
this.groupsData.setData(null);
}
this.groupsData = newDataSet;
this.itemSet.setGroups(newDataSet);
}
/**
* Set both items and groups in one go
* @param {{items: (Array | vis.DataSet), groups: (Array | vis.DataSet)}} data
*/
setData(data) {
if (data && data.groups) {
this.setGroups(data.groups);
}
if (data && data.items) {
this.setItems(data.items);
}
}
/**
* Set selected items by their id. Replaces the current selection
* Unknown id's are silently ignored.
* @param {string[] | string} [ids] An array with zero or more id's of the items to be
* selected. If ids is an empty array, all items will be
* unselected.
* @param {Object} [options] Available options:
* `focus: boolean`
* If true, focus will be set to the selected item(s)
* `animation: boolean | {duration: number, easingFunction: string}`
* If true (default), the range is animated
* smoothly to the new window. An object can be
* provided to specify duration and easing function.
* Default duration is 500 ms, and default easing
* function is 'easeInOutQuad'.
* Only applicable when option focus is true.
*/
setSelection(ids, options) {
this.itemSet && this.itemSet.setSelection(ids);
if (options && options.focus) {
this.focus(ids, options);
}
}
/**
* Get the selected items by their id
* @return {Array} ids The ids of the selected items
*/
getSelection() {
return this.itemSet && this.itemSet.getSelection() || [];
}
/**
* Adjust the visible window such that the selected item (or multiple items)
* are centered on screen.
* @param {string | String[]} id An item id or array with item ids
* @param {Object} [options] Available options:
* `animation: boolean | {duration: number, easingFunction: string}`
* If true (default), the range is animated
* smoothly to the new window. An object can be
* provided to specify duration and easing function.
* Default duration is 500 ms, and default easing
* function is 'easeInOutQuad'.
* `zoom: boolean`
* If true (default), the timeline will
* zoom on the element after focus it.
*/
focus(id, options) {
if (!this.itemsData || id == undefined) return;
const ids = Array.isArray(id) ? id : [id];
// get the specified item(s)
const itemsData = this.itemsData.get(ids);
// calculate minimum start and maximum end of specified items
let start = null;
let end = null;
itemsData.forEach(itemData => {
const s = itemData.start.valueOf();
const e = 'end' in itemData ? itemData.end.valueOf() : itemData.start.valueOf();
if (start === null || s < start) {
start = s;
}
if (end === null || e > end) {
end = e;
}
});
if (start !== null && end !== null) {
const me = this;
// Use the first item for the vertical focus
const item = this.itemSet.items[ids[0]];
let startPos = this._getScrollTop() * -1;
let initialVerticalScroll = null;
// Setup a handler for each frame of the vertical scroll
const verticalAnimationFrame = (ease, willDraw, done) => {
const verticalScroll = getItemVerticalScroll(me, item);
if (verticalScroll === false) {
return; // We don't need to scroll, so do nothing
}
if(!initialVerticalScroll) {
initialVerticalScroll = verticalScroll;
}
if(initialVerticalScroll.itemTop == verticalScroll.itemTop && !initialVerticalScroll.shouldScroll) {
return; // We don't need to scroll, so do nothing
}
else if(initialVerticalScroll.itemTop != verticalScroll.itemTop && verticalScroll.shouldScroll) {
// The redraw shifted elements, so reset the animation to correct
initialVerticalScroll = verticalScroll;
startPos = me._getScrollTop() * -1;
}
const from = startPos;
const to = initialVerticalScroll.scrollOffset;
const scrollTop = done ? to : (from + (to - from) * ease);
me._setScrollTop(-scrollTop);
if(!willDraw) {
me._redraw();
}
};
// Enforces the final vertical scroll position
const setFinalVerticalPosition = () => {
const finalVerticalScroll = getItemVerticalScroll(me, item);
if (finalVerticalScroll.shouldScroll && finalVerticalScroll.itemTop != initialVerticalScroll.itemTop) {
me._setScrollTop(-finalVerticalScroll.scrollOffset);
me._redraw();
}
};
// Perform one last check at the end to make sure the final vertical
// position is correct
const finalVerticalCallback = () => {
// Double check we ended at the proper scroll position
setFinalVerticalPosition();
// Let the redraw settle and finalize the position.
setTimeout(setFinalVerticalPosition, 100);
};
// calculate the new middle and interval for the window
const zoom = options && options.zoom !== undefined ? options.zoom : true;
const middle = (start + end) / 2;
const interval = zoom ? (end - start) * 1.1 : Math.max(this.range.end - this.range.start, (end - start) * 1.1);
const animation = options && options.animation !== undefined ? options.animation : true;
if (!animation) {
// We aren't animating so set a default so that the final callback forces the vertical location
initialVerticalScroll = { shouldScroll: false, scrollOffset: -1, itemTop: -1 };
}
this.range.setRange(middle - interval / 2, middle + interval / 2, { animation }, finalVerticalCallback, verticalAnimationFrame);
}
}
/**
* Set Timeline window such that it fits all items
* @param {Object} [options] Available options:
* `animation: boolean | {duration: number, easingFunction: string}`
* If true (default), the range is animated
* smoothly to the new window. An object can be
* provided to specify duration and easing function.
* Default duration is 500 ms, and default easing
* function is 'easeInOutQuad'.
* @param {function} [callback]
*/
fit(options, callback) {
const animation = (options && options.animation !== undefined) ? options.animation : true;
let range;
if (this.itemsData.length === 1 && this.itemsData.get()[0].end === undefined) {
// a single item -> don't fit, just show a range around the item from -4 to +3 days
range = this.getDataRange();
this.moveTo(range.min.valueOf(), {animation}, callback);
}
else {
// exactly fit the items (plus a small margin)
range = this.getItemRange();
this.range.setRange(range.min, range.max, { animation }, callback);
}
}
/**
* Determine the range of the items, taking into account their actual width
* and a margin of 10 pixels on both sides.
*
* @returns {{min: Date, max: Date}}
*/
getItemRange() {
// get a rough approximation for the range based on the items start and end dates
const range = this.getDataRange();
let min = range.min !== null ? range.min.valueOf() : null;
let max = range.max !== null ? range.max.valueOf() : null;
let minItem = null;
let maxItem = null;
if (min != null && max != null) {
let interval = (max - min); // ms
if (interval <= 0) {
interval = 10;
}
const factor = interval / this.props.center.width;
const redrawQueue = {};
let redrawQueueLength = 0;
// collect redraw functions
util$3.forEach(this.itemSet.items, (item, key) => {
if (item.groupShowing) {
const returnQueue = true;
redrawQueue[key] = item.redraw(returnQueue);
redrawQueueLength = redrawQueue[key].length;
}
});
const needRedraw = redrawQueueLength > 0;
if (needRedraw) {
// redraw all regular items
for (let i = 0; i < redrawQueueLength; i++) {
util$3.forEach(redrawQueue, fns => {
fns[i]();
});
}
}
// calculate the date of the left side and right side of the items given
util$3.forEach(this.itemSet.items, item => {
const start = getStart(item);
const end = getEnd(item);
let startSide;
let endSide;
if (this.options.rtl) {
startSide = start - (item.getWidthRight() + 10) * factor;
endSide = end + (item.getWidthLeft() + 10) * factor;
} else {
startSide = start - (item.getWidthLeft() + 10) * factor;
endSide = end + (item.getWidthRight() + 10) * factor;
}
if (startSide < min) {
min = startSide;
minItem = item;
}
if (endSide > max) {
max = endSide;
maxItem = item;
}
});
if (minItem && maxItem) {
const lhs = minItem.getWidthLeft() + 10;
const rhs = maxItem.getWidthRight() + 10;
const delta = this.props.center.width - lhs - rhs; // px
if (delta > 0) {
if (this.options.rtl) {
min = getStart(minItem) - rhs * interval / delta; // ms
max = getEnd(maxItem) + lhs * interval / delta; // ms
} else {
min = getStart(minItem) - lhs * interval / delta; // ms
max = getEnd(maxItem) + rhs * interval / delta; // ms
}
}
}
}
return {
min: min != null ? new Date(min) : null,
max: max != null ? new Date(max) : null
}
}
/**
* Calculate the data range of the items start and end dates
* @returns {{min: Date, max: Date}}
*/
getDataRange() {
let min = null;
let max = null;
if (this.itemsData) {
this.itemsData.forEach(item => {
const start = util$3.convert(item.start, 'Date').valueOf();
const end = util$3.convert(item.end != undefined ? item.end : item.start, 'Date').valueOf();
if (min === null || start < min) {
min = start;
}
if (max === null || end > max) {
max = end;
}
});
}
return {
min: min != null ? new Date(min) : null,
max: max != null ? new Date(max) : null
}
}
/**
* Generate Timeline related information from an event
* @param {Event} event
* @return {Object} An object with related information, like on which area
* The event happened, whether clicked on an item, etc.
*/
getEventProperties(event) {
const clientX = event.center ? event.center.x : event.clientX;
const clientY = event.center ? event.center.y : event.clientY;
const centerContainerRect = this.dom.centerContainer.getBoundingClientRect();
const x = this.options.rtl ? centerContainerRect.right - clientX : clientX - centerContainerRect.left;
const y = clientY - centerContainerRect.top;
const item = this.itemSet.itemFromTarget(event);
const group = this.itemSet.groupFromTarget(event);
const customTime = CustomTime.customTimeFromTarget(event);
const snap = this.itemSet.options.snap || null;
const scale = this.body.util.getScale();
const step = this.body.util.getStep();
const time = this._toTime(x);
const snappedTime = snap ? snap(time, scale, step) : time;
const element = util$3.getTarget(event);
let what = null;
if (item != null) {what = 'item';}
else if (customTime != null) {what = 'custom-time';}
else if (util$3.hasParent(element, this.timeAxis.dom.foreground)) {what = 'axis';}
else if (this.timeAxis2 && util$3.hasParent(element, this.timeAxis2.dom.foreground)) {what = 'axis';}
else if (util$3.hasParent(element, this.itemSet.dom.labelSet)) {what = 'group-label';}
else if (util$3.hasParent(element, this.currentTime.bar)) {what = 'current-time';}
else if (util$3.hasParent(element, this.dom.center)) {what = 'background';}
return {
event,
item: item ? item.id : null,
isCluster: item ? !!item.isCluster: false,
items: item ? item.items || []: null,
group: group ? group.groupId : null,
customTime: customTime ? customTime.options.id : null,
what,
pageX: event.srcEvent ? event.srcEvent.pageX : event.pageX,
pageY: event.srcEvent ? event.srcEvent.pageY : event.pageY,
x,
y,
time,
snappedTime
}
}
/**
* Toggle Timeline rolling mode
*/
toggleRollingMode() {
if (this.range.rolling) {
this.range.stopRolling();
} else {
if (this.options.rollingMode == undefined) {
this.setOptions(this.options);
}
this.range.startRolling();
}
}
/**
* redraw
* @private
*/
_redraw() {
Core.prototype._redraw.call(this);
}
/**
* on fit callback
* @param {object} args
* @private
*/
_onFit(args) {
const { start, end, animation } = args;
if (!end) {
this.moveTo(start.valueOf(), {
animation
});
} else {
this.range.setRange(start, end, {
animation: animation
});
}
}
}
/**
*
* @param {timeline.Item} item
* @returns {number}
*/
function getStart(item) {
return util$3.convert(item.data.start, 'Date').valueOf()
}
/**
*
* @param {timeline.Item} item
* @returns {number}
*/
function getEnd(item) {
const end = item.data.end != undefined ? item.data.end : item.data.start;
return util$3.convert(end, 'Date').valueOf();
}
/**
* @param {vis.Timeline} timeline
* @param {timeline.Item} item
* @return {{shouldScroll: bool, scrollOffset: number, itemTop: number}}
*/
function getItemVerticalScroll(timeline, item) {
if (!item.parent) {
// The item no longer exists, so ignore this focus.
return false;
}
const itemsetHeight = timeline.options.rtl ? timeline.props.rightContainer.height : timeline.props.leftContainer.height;
const contentHeight = timeline.props.center.height;
const group = item.parent;
let offset = group.top;
let shouldScroll = true;
const orientation = timeline.timeAxis.options.orientation.axis;
const itemTop = () => {
if (orientation == "bottom") {
return group.height - item.top - item.height;
}
else {
return item.top;
}
};
const currentScrollHeight = timeline._getScrollTop() * -1;
const targetOffset = offset + itemTop();
const height = item.height;
if (targetOffset < currentScrollHeight) {
if (offset + itemsetHeight <= offset + itemTop() + height) {
offset += itemTop() - timeline.itemSet.options.margin.item.vertical;
}
}
else if (targetOffset + height > currentScrollHeight + itemsetHeight) {
offset += itemTop() + height - itemsetHeight + timeline.itemSet.options.margin.item.vertical;
}
else {
shouldScroll = false;
}
offset = Math.min(offset, contentHeight - itemsetHeight);
return { shouldScroll, scrollOffset: offset, itemTop: targetOffset };
}
// DOM utility methods
/**
* this prepares the JSON container for allocating SVG elements
* @param {Object} JSONcontainer
* @private
*/
function prepareElements(JSONcontainer) {
// cleanup the redundant svgElements;
for (var elementType in JSONcontainer) {
if (JSONcontainer.hasOwnProperty(elementType)) {
JSONcontainer[elementType].redundant = JSONcontainer[elementType].used;
JSONcontainer[elementType].used = [];
}
}
}
/**
* this cleans up all the unused SVG elements. By asking for the parentNode, we only need to supply the JSON container from
* which to remove the redundant elements.
*
* @param {Object} JSONcontainer
* @private
*/
function cleanupElements(JSONcontainer) {
// cleanup the redundant svgElements;
for (var elementType in JSONcontainer) {
if (JSONcontainer.hasOwnProperty(elementType)) {
if (JSONcontainer[elementType].redundant) {
for (var i = 0; i < JSONcontainer[elementType].redundant.length; i++) {
JSONcontainer[elementType].redundant[i].parentNode.removeChild(JSONcontainer[elementType].redundant[i]);
}
JSONcontainer[elementType].redundant = [];
}
}
}
}
/**
* Ensures that all elements are removed first up so they can be recreated cleanly
* @param {Object} JSONcontainer
*/
function resetElements(JSONcontainer) {
prepareElements(JSONcontainer);
cleanupElements(JSONcontainer);
prepareElements(JSONcontainer);
}
/**
* Allocate or generate an SVG element if needed. Store a reference to it in the JSON container and draw it in the svgContainer
* the JSON container and the SVG container have to be supplied so other svg containers (like the legend) can use this.
*
* @param {string} elementType
* @param {Object} JSONcontainer
* @param {Object} svgContainer
* @returns {Element}
* @private
*/
function getSVGElement(elementType, JSONcontainer, svgContainer) {
var element;
// allocate SVG element, if it doesnt yet exist, create one.
if (JSONcontainer.hasOwnProperty(elementType)) { // this element has been created before
// check if there is an redundant element
if (JSONcontainer[elementType].redundant.length > 0) {
element = JSONcontainer[elementType].redundant[0];
JSONcontainer[elementType].redundant.shift();
}
else {
// create a new element and add it to the SVG
element = document.createElementNS('http://www.w3.org/2000/svg', elementType);
svgContainer.appendChild(element);
}
}
else {
// create a new element and add it to the SVG, also create a new object in the svgElements to keep track of it.
element = document.createElementNS('http://www.w3.org/2000/svg', elementType);
JSONcontainer[elementType] = {used: [], redundant: []};
svgContainer.appendChild(element);
}
JSONcontainer[elementType].used.push(element);
return element;
}
/**
* Allocate or generate an SVG element if needed. Store a reference to it in the JSON container and draw it in the svgContainer
* the JSON container and the SVG container have to be supplied so other svg containers (like the legend) can use this.
*
* @param {string} elementType
* @param {Object} JSONcontainer
* @param {Element} DOMContainer
* @param {Element} insertBefore
* @returns {*}
*/
function getDOMElement(elementType, JSONcontainer, DOMContainer, insertBefore) {
var element;
// allocate DOM element, if it doesnt yet exist, create one.
if (JSONcontainer.hasOwnProperty(elementType)) { // this element has been created before
// check if there is an redundant element
if (JSONcontainer[elementType].redundant.length > 0) {
element = JSONcontainer[elementType].redundant[0];
JSONcontainer[elementType].redundant.shift();
}
else {
// create a new element and add it to the SVG
element = document.createElement(elementType);
if (insertBefore !== undefined) {
DOMContainer.insertBefore(element, insertBefore);
}
else {
DOMContainer.appendChild(element);
}
}
}
else {
// create a new element and add it to the SVG, also create a new object in the svgElements to keep track of it.
element = document.createElement(elementType);
JSONcontainer[elementType] = {used: [], redundant: []};
if (insertBefore !== undefined) {
DOMContainer.insertBefore(element, insertBefore);
}
else {
DOMContainer.appendChild(element);
}
}
JSONcontainer[elementType].used.push(element);
return element;
}
/**
* Draw a point object. This is a separate function because it can also be called by the legend.
* The reason the JSONcontainer and the target SVG svgContainer have to be supplied is so the legend can use these functions
* as well.
*
* @param {number} x
* @param {number} y
* @param {Object} groupTemplate: A template containing the necessary information to draw the datapoint e.g., {style: 'circle', size: 5, className: 'className' }
* @param {Object} JSONcontainer
* @param {Object} svgContainer
* @param {Object} labelObj
* @returns {vis.PointItem}
*/
function drawPoint(x, y, groupTemplate, JSONcontainer, svgContainer, labelObj) {
var point;
if (groupTemplate.style == 'circle') {
point = getSVGElement('circle', JSONcontainer, svgContainer);
point.setAttributeNS(null, "cx", x);
point.setAttributeNS(null, "cy", y);
point.setAttributeNS(null, "r", 0.5 * groupTemplate.size);
}
else {
point = getSVGElement('rect', JSONcontainer, svgContainer);
point.setAttributeNS(null, "x", x - 0.5 * groupTemplate.size);
point.setAttributeNS(null, "y", y - 0.5 * groupTemplate.size);
point.setAttributeNS(null, "width", groupTemplate.size);
point.setAttributeNS(null, "height", groupTemplate.size);
}
if (groupTemplate.styles !== undefined) {
point.setAttributeNS(null, "style", groupTemplate.styles);
}
point.setAttributeNS(null, "class", groupTemplate.className + " vis-point");
//handle label
if (labelObj) {
var label = getSVGElement('text', JSONcontainer, svgContainer);
if (labelObj.xOffset) {
x = x + labelObj.xOffset;
}
if (labelObj.yOffset) {
y = y + labelObj.yOffset;
}
if (labelObj.content) {
label.textContent = labelObj.content;
}
if (labelObj.className) {
label.setAttributeNS(null, "class", labelObj.className + " vis-label");
}
label.setAttributeNS(null, "x", x);
label.setAttributeNS(null, "y", y);
}
return point;
}
/**
* draw a bar SVG element centered on the X coordinate
*
* @param {number} x
* @param {number} y
* @param {number} width
* @param {number} height
* @param {string} className
* @param {Object} JSONcontainer
* @param {Object} svgContainer
* @param {string} style
*/
function drawBar (x, y, width, height, className, JSONcontainer, svgContainer, style) {
if (height != 0) {
if (height < 0) {
height *= -1;
y -= height;
}
var rect = getSVGElement('rect',JSONcontainer, svgContainer);
rect.setAttributeNS(null, "x", x - 0.5 * width);
rect.setAttributeNS(null, "y", y);
rect.setAttributeNS(null, "width", width);
rect.setAttributeNS(null, "height", height);
rect.setAttributeNS(null, "class", className);
if (style) {
rect.setAttributeNS(null, "style", style);
}
}
}
/**
* get default language
* @returns {string}
*/
function getNavigatorLanguage() {
try {
if (!navigator) return 'en';
if (navigator.languages && navigator.languages.length) {
return navigator.languages;
} else {
return navigator.userLanguage || navigator.language || navigator.browserLanguage || 'en';
}
}
catch(error) {
return 'en';
}
}
/** DataScale */
class DataScale {
/**
*
* @param {number} start
* @param {number} end
* @param {boolean} autoScaleStart
* @param {boolean} autoScaleEnd
* @param {number} containerHeight
* @param {number} majorCharHeight
* @param {boolean} zeroAlign
* @param {function} formattingFunction
* @constructor DataScale
*/
constructor(
start,
end,
autoScaleStart,
autoScaleEnd,
containerHeight,
majorCharHeight,
zeroAlign = false,
formattingFunction=false) {
this.majorSteps = [1, 2, 5, 10];
this.minorSteps = [0.25, 0.5, 1, 2];
this.customLines = null;
this.containerHeight = containerHeight;
this.majorCharHeight = majorCharHeight;
this._start = start;
this._end = end;
this.scale = 1;
this.minorStepIdx = -1;
this.magnitudefactor = 1;
this.determineScale();
this.zeroAlign = zeroAlign;
this.autoScaleStart = autoScaleStart;
this.autoScaleEnd = autoScaleEnd;
this.formattingFunction = formattingFunction;
if (autoScaleStart || autoScaleEnd) {
const me = this;
const roundToMinor = value => {
const rounded = value - (value % (me.magnitudefactor * me.minorSteps[me.minorStepIdx]));
if (value % (me.magnitudefactor * me.minorSteps[me.minorStepIdx]) > 0.5 * (me.magnitudefactor * me.minorSteps[me.minorStepIdx])) {
return rounded + (me.magnitudefactor * me.minorSteps[me.minorStepIdx]);
}
else {
return rounded;
}
};
if (autoScaleStart) {
this._start -= this.magnitudefactor * 2 * this.minorSteps[this.minorStepIdx];
this._start = roundToMinor(this._start);
}
if (autoScaleEnd) {
this._end += this.magnitudefactor * this.minorSteps[this.minorStepIdx];
this._end = roundToMinor(this._end);
}
this.determineScale();
}
}
/**
* set chart height
* @param {number} majorCharHeight
*/
setCharHeight(majorCharHeight) {
this.majorCharHeight = majorCharHeight;
}
/**
* set height
* @param {number} containerHeight
*/
setHeight(containerHeight) {
this.containerHeight = containerHeight;
}
/**
* determine scale
*/
determineScale() {
const range = this._end - this._start;
this.scale = this.containerHeight / range;
const minimumStepValue = this.majorCharHeight / this.scale;
const orderOfMagnitude = (range > 0)
? Math.round(Math.log(range) / Math.LN10)
: 0;
this.minorStepIdx = -1;
this.magnitudefactor = Math.pow(10, orderOfMagnitude);
let start = 0;
if (orderOfMagnitude < 0) {
start = orderOfMagnitude;
}
let solutionFound = false;
for (let l = start; Math.abs(l) <= Math.abs(orderOfMagnitude); l++) {
this.magnitudefactor = Math.pow(10, l);
for (let j = 0; j < this.minorSteps.length; j++) {
const stepSize = this.magnitudefactor * this.minorSteps[j];
if (stepSize >= minimumStepValue) {
solutionFound = true;
this.minorStepIdx = j;
break;
}
}
if (solutionFound === true) {
break;
}
}
}
/**
* returns if value is major
* @param {number} value
* @returns {boolean}
*/
is_major(value) {
return (value % (this.magnitudefactor * this.majorSteps[this.minorStepIdx]) === 0);
}
/**
* returns step size
* @returns {number}
*/
getStep() {
return this.magnitudefactor * this.minorSteps[this.minorStepIdx];
}
/**
* returns first major
* @returns {number}
*/
getFirstMajor() {
const majorStep = this.magnitudefactor * this.majorSteps[this.minorStepIdx];
return this.convertValue(this._start + ((majorStep - (this._start % majorStep)) % majorStep));
}
/**
* returns first major
* @param {date} current
* @returns {date} formatted date
*/
formatValue(current) {
let returnValue = current.toPrecision(5);
if (typeof this.formattingFunction === 'function') {
returnValue = this.formattingFunction(current);
}
if (typeof returnValue === 'number') {
return `${returnValue}`;
}
else if (typeof returnValue === 'string') {
return returnValue;
}
else {
return current.toPrecision(5);
}
}
/**
* returns lines
* @returns {object} lines
*/
getLines() {
const lines = [];
const step = this.getStep();
const bottomOffset = (step - (this._start % step)) % step;
for (let i = (this._start + bottomOffset); this._end-i > 0.00001; i += step) {
if (i != this._start) { //Skip the bottom line
lines.push({major: this.is_major(i), y: this.convertValue(i), val: this.formatValue(i)});
}
}
return lines;
}
/**
* follow scale
* @param {object} other
*/
followScale(other) {
const oldStepIdx = this.minorStepIdx;
const oldStart = this._start;
const oldEnd = this._end;
const me = this;
const increaseMagnitude = () => {
me.magnitudefactor *= 2;
};
const decreaseMagnitude = () => {
me.magnitudefactor /= 2;
};
if ((other.minorStepIdx <= 1 && this.minorStepIdx <= 1) || (other.minorStepIdx > 1 && this.minorStepIdx > 1)) ; else if (other.minorStepIdx < this.minorStepIdx) {
//I'm 5, they are 4 per major.
this.minorStepIdx = 1;
if (oldStepIdx == 2) {
increaseMagnitude();
} else {
increaseMagnitude();
increaseMagnitude();
}
} else {
//I'm 4, they are 5 per major
this.minorStepIdx = 2;
if (oldStepIdx == 1) {
decreaseMagnitude();
} else {
decreaseMagnitude();
decreaseMagnitude();
}
}
//Get masters stats:
const otherZero = other.convertValue(0);
const otherStep = other.getStep() * other.scale;
let done = false;
let count = 0;
//Loop until magnitude is correct for given constrains.
while (!done && count++ <5) {
//Get my stats:
this.scale = otherStep / (this.minorSteps[this.minorStepIdx] * this.magnitudefactor);
const newRange = this.containerHeight / this.scale;
//For the case the magnitudefactor has changed:
this._start = oldStart;
this._end = this._start + newRange;
const myOriginalZero = this._end * this.scale;
const majorStep = this.magnitudefactor * this.majorSteps[this.minorStepIdx];
const majorOffset = this.getFirstMajor() - other.getFirstMajor();
if (this.zeroAlign) {
const zeroOffset = otherZero - myOriginalZero;
this._end += (zeroOffset / this.scale);
this._start = this._end - newRange;
} else {
if (!this.autoScaleStart) {
this._start += majorStep - (majorOffset / this.scale);
this._end = this._start + newRange;
} else {
this._start -= majorOffset / this.scale;
this._end = this._start + newRange;
}
}
if (!this.autoScaleEnd && this._end > oldEnd+0.00001) {
//Need to decrease magnitude to prevent scale overshoot! (end)
decreaseMagnitude();
done = false;
continue;
}
if (!this.autoScaleStart && this._start < oldStart-0.00001) {
if (this.zeroAlign && oldStart >= 0) {
console.warn("Can't adhere to given 'min' range, due to zeroalign");
} else {
//Need to decrease magnitude to prevent scale overshoot! (start)
decreaseMagnitude();
done = false;
continue;
}
}
if (this.autoScaleStart && this.autoScaleEnd && newRange < (oldEnd-oldStart)){
increaseMagnitude();
done = false;
continue;
}
done = true;
}
}
/**
* convert value
* @param {number} value
* @returns {number}
*/
convertValue(value) {
return this.containerHeight - ((value - this._start) * this.scale);
}
/**
* returns screen to value
* @param {number} pixels
* @returns {number}
*/
screenToValue(pixels) {
return ((this.containerHeight - pixels) / this.scale) + this._start;
}
}
/** A horizontal time axis */
class DataAxis extends Component {
/**
* @param {Object} body
* @param {Object} [options] See DataAxis.setOptions for the available
* options.
* @param {SVGElement} svg
* @param {timeline.LineGraph.options} linegraphOptions
* @constructor DataAxis
* @extends Component
*/
constructor(body, options, svg, linegraphOptions) {
super();
this.id = dist.v4();
this.body = body;
this.defaultOptions = {
orientation: 'left', // supported: 'left', 'right'
showMinorLabels: true,
showMajorLabels: true,
showWeekScale: false,
icons: false,
majorLinesOffset: 7,
minorLinesOffset: 4,
labelOffsetX: 10,
labelOffsetY: 2,
iconWidth: 20,
width: '40px',
visible: true,
alignZeros: true,
left: {
range: {min: undefined, max: undefined},
format(value) {
return `${parseFloat(value.toPrecision(3))}`;
},
title: {text: undefined, style: undefined}
},
right: {
range: {min: undefined, max: undefined},
format(value) {
return `${parseFloat(value.toPrecision(3))}`;
},
title: {text: undefined, style: undefined}
}
};
this.linegraphOptions = linegraphOptions;
this.linegraphSVG = svg;
this.props = {};
this.DOMelements = { // dynamic elements
lines: {},
labels: {},
title: {}
};
this.dom = {};
this.scale = undefined;
this.range = {start: 0, end: 0};
this.options = util$3.extend({}, this.defaultOptions);
this.conversionFactor = 1;
this.setOptions(options);
this.width = Number((`${this.options.width}`).replace("px", ""));
this.minWidth = this.width;
this.height = this.linegraphSVG.getBoundingClientRect().height;
this.hidden = false;
this.stepPixels = 25;
this.zeroCrossing = -1;
this.amountOfSteps = -1;
this.lineOffset = 0;
this.master = true;
this.masterAxis = null;
this.svgElements = {};
this.iconsRemoved = false;
this.groups = {};
this.amountOfGroups = 0;
// create the HTML DOM
this._create();
if (this.scale == undefined) {
this._redrawLabels();
}
this.framework = {svg: this.svg, svgElements: this.svgElements, options: this.options, groups: this.groups};
const me = this;
this.body.emitter.on("verticalDrag", () => {
me.dom.lineContainer.style.top = `${me.body.domProps.scrollTop}px`;
});
}
/**
* Adds group to data axis
* @param {string} label
* @param {object} graphOptions
*/
addGroup(label, graphOptions) {
if (!this.groups.hasOwnProperty(label)) {
this.groups[label] = graphOptions;
}
this.amountOfGroups += 1;
}
/**
* updates group of data axis
* @param {string} label
* @param {object} graphOptions
*/
updateGroup(label, graphOptions) {
if (!this.groups.hasOwnProperty(label)) {
this.amountOfGroups += 1;
}
this.groups[label] = graphOptions;
}
/**
* removes group of data axis
* @param {string} label
*/
removeGroup(label) {
if (this.groups.hasOwnProperty(label)) {
delete this.groups[label];
this.amountOfGroups -= 1;
}
}
/**
* sets options
* @param {object} options
*/
setOptions(options) {
if (options) {
let redraw = false;
if (this.options.orientation != options.orientation && options.orientation !== undefined) {
redraw = true;
}
const fields = [
'orientation',
'showMinorLabels',
'showMajorLabels',
'icons',
'majorLinesOffset',
'minorLinesOffset',
'labelOffsetX',
'labelOffsetY',
'iconWidth',
'width',
'visible',
'left',
'right',
'alignZeros'
];
util$3.selectiveDeepExtend(fields, this.options, options);
this.minWidth = Number((`${this.options.width}`).replace("px", ""));
if (redraw === true && this.dom.frame) {
this.hide();
this.show();
}
}
}
/**
* Create the HTML DOM for the DataAxis
*/
_create() {
this.dom.frame = document.createElement('div');
this.dom.frame.style.width = this.options.width;
this.dom.frame.style.height = this.height;
this.dom.lineContainer = document.createElement('div');
this.dom.lineContainer.style.width = '100%';
this.dom.lineContainer.style.height = this.height;
this.dom.lineContainer.style.position = 'relative';
this.dom.lineContainer.style.visibility = 'visible';
this.dom.lineContainer.style.display = 'block';
// create svg element for graph drawing.
this.svg = document.createElementNS('http://www.w3.org/2000/svg', "svg");
this.svg.style.position = "absolute";
this.svg.style.top = '0px';
this.svg.style.height = '100%';
this.svg.style.width = '100%';
this.svg.style.display = "block";
this.dom.frame.appendChild(this.svg);
}
/**
* redraws groups icons
*/
_redrawGroupIcons() {
prepareElements(this.svgElements);
let x;
const iconWidth = this.options.iconWidth;
const iconHeight = 15;
const iconOffset = 4;
let y = iconOffset + 0.5 * iconHeight;
if (this.options.orientation === 'left') {
x = iconOffset;
}
else {
x = this.width - iconWidth - iconOffset;
}
const groupArray = Object.keys(this.groups);
groupArray.sort((a, b) => a < b ? -1 : 1);
for (const groupId of groupArray) {
if (this.groups[groupId].visible === true && (this.linegraphOptions.visibility[groupId] === undefined || this.linegraphOptions.visibility[groupId] === true)) {
this.groups[groupId].getLegend(iconWidth, iconHeight, this.framework, x, y);
y += iconHeight + iconOffset;
}
}
cleanupElements(this.svgElements);
this.iconsRemoved = false;
}
/**
* Cleans up icons
*/
_cleanupIcons() {
if (this.iconsRemoved === false) {
prepareElements(this.svgElements);
cleanupElements(this.svgElements);
this.iconsRemoved = true;
}
}
/**
* Create the HTML DOM for the DataAxis
*/
show() {
this.hidden = false;
if (!this.dom.frame.parentNode) {
if (this.options.orientation === 'left') {
this.body.dom.left.appendChild(this.dom.frame);
}
else {
this.body.dom.right.appendChild(this.dom.frame);
}
}
if (!this.dom.lineContainer.parentNode) {
this.body.dom.backgroundHorizontal.appendChild(this.dom.lineContainer);
}
this.dom.lineContainer.style.display = 'block';
}
/**
* Create the HTML DOM for the DataAxis
*/
hide() {
this.hidden = true;
if (this.dom.frame.parentNode) {
this.dom.frame.parentNode.removeChild(this.dom.frame);
}
this.dom.lineContainer.style.display = 'none';
}
/**
* Set a range (start and end)
* @param {number} start
* @param {number} end
*/
setRange(start, end) {
this.range.start = start;
this.range.end = end;
}
/**
* Repaint the component
* @return {boolean} Returns true if the component is resized
*/
redraw() {
let resized = false;
let activeGroups = 0;
// Make sure the line container adheres to the vertical scrolling.
this.dom.lineContainer.style.top = `${this.body.domProps.scrollTop}px`;
for (const groupId in this.groups) {
if (this.groups.hasOwnProperty(groupId)) {
if (this.groups[groupId].visible === true && (this.linegraphOptions.visibility[groupId] === undefined || this.linegraphOptions.visibility[groupId] === true)) {
activeGroups++;
}
}
}
if (this.amountOfGroups === 0 || activeGroups === 0) {
this.hide();
}
else {
this.show();
this.height = Number(this.linegraphSVG.style.height.replace("px", ""));
// svg offsetheight did not work in firefox and explorer...
this.dom.lineContainer.style.height = `${this.height}px`;
this.width = this.options.visible === true ? Number((`${this.options.width}`).replace("px", "")) : 0;
const props = this.props;
const frame = this.dom.frame;
// update classname
frame.className = 'vis-data-axis';
// calculate character width and height
this._calculateCharSize();
const orientation = this.options.orientation;
const showMinorLabels = this.options.showMinorLabels;
const showMajorLabels = this.options.showMajorLabels;
const backgroundHorizontalOffsetWidth = this.body.dom.backgroundHorizontal.offsetWidth;
// determine the width and height of the elements for the axis
props.minorLabelHeight = showMinorLabels ? props.minorCharHeight : 0;
props.majorLabelHeight = showMajorLabels ? props.majorCharHeight : 0;
props.minorLineWidth = backgroundHorizontalOffsetWidth - this.lineOffset - this.width + 2 * this.options.minorLinesOffset;
props.minorLineHeight = 1;
props.majorLineWidth = backgroundHorizontalOffsetWidth - this.lineOffset - this.width + 2 * this.options.majorLinesOffset;
props.majorLineHeight = 1;
// take frame offline while updating (is almost twice as fast)
if (orientation === 'left') {
frame.style.top = '0';
frame.style.left = '0';
frame.style.bottom = '';
frame.style.width = `${this.width}px`;
frame.style.height = `${this.height}px`;
this.props.width = this.body.domProps.left.width;
this.props.height = this.body.domProps.left.height;
}
else { // right
frame.style.top = '';
frame.style.bottom = '0';
frame.style.left = '0';
frame.style.width = `${this.width}px`;
frame.style.height = `${this.height}px`;
this.props.width = this.body.domProps.right.width;
this.props.height = this.body.domProps.right.height;
}
resized = this._redrawLabels();
resized = this._isResized() || resized;
if (this.options.icons === true) {
this._redrawGroupIcons();
}
else {
this._cleanupIcons();
}
this._redrawTitle(orientation);
}
return resized;
}
/**
* Repaint major and minor text labels and vertical grid lines
*
* @returns {boolean}
* @private
*/
_redrawLabels() {
let resized = false;
prepareElements(this.DOMelements.lines);
prepareElements(this.DOMelements.labels);
const orientation = this.options['orientation'];
const customRange = this.options[orientation].range != undefined ? this.options[orientation].range : {};
//Override range with manual options:
let autoScaleEnd = true;
if (customRange.max != undefined) {
this.range.end = customRange.max;
autoScaleEnd = false;
}
let autoScaleStart = true;
if (customRange.min != undefined) {
this.range.start = customRange.min;
autoScaleStart = false;
}
this.scale = new DataScale(
this.range.start,
this.range.end,
autoScaleStart,
autoScaleEnd,
this.dom.frame.offsetHeight,
this.props.majorCharHeight,
this.options.alignZeros,
this.options[orientation].format
);
if (this.master === false && this.masterAxis != undefined) {
this.scale.followScale(this.masterAxis.scale);
this.dom.lineContainer.style.display = 'none';
} else {
this.dom.lineContainer.style.display = 'block';
}
//Is updated in side-effect of _redrawLabel():
this.maxLabelSize = 0;
const lines = this.scale.getLines();
lines.forEach(
line=> {
const y = line.y;
const isMajor = line.major;
if (this.options['showMinorLabels'] && isMajor === false) {
this._redrawLabel(y - 2, line.val, orientation, 'vis-y-axis vis-minor', this.props.minorCharHeight);
}
if (isMajor) {
if (y >= 0) {
this._redrawLabel(y - 2, line.val, orientation, 'vis-y-axis vis-major', this.props.majorCharHeight);
}
}
if (this.master === true) {
if (isMajor) {
this._redrawLine(y, orientation, 'vis-grid vis-horizontal vis-major', this.options.majorLinesOffset, this.props.majorLineWidth);
}
else {
this._redrawLine(y, orientation, 'vis-grid vis-horizontal vis-minor', this.options.minorLinesOffset, this.props.minorLineWidth);
}
}
});
// Note that title is rotated, so we're using the height, not width!
let titleWidth = 0;
if (this.options[orientation].title !== undefined && this.options[orientation].title.text !== undefined) {
titleWidth = this.props.titleCharHeight;
}
const offset = this.options.icons === true ? Math.max(this.options.iconWidth, titleWidth) + this.options.labelOffsetX + 15 : titleWidth + this.options.labelOffsetX + 15;
// this will resize the yAxis to accommodate the labels.
if (this.maxLabelSize > (this.width - offset) && this.options.visible === true) {
this.width = this.maxLabelSize + offset;
this.options.width = `${this.width}px`;
cleanupElements(this.DOMelements.lines);
cleanupElements(this.DOMelements.labels);
this.redraw();
resized = true;
}
// this will resize the yAxis if it is too big for the labels.
else if (this.maxLabelSize < (this.width - offset) && this.options.visible === true && this.width > this.minWidth) {
this.width = Math.max(this.minWidth, this.maxLabelSize + offset);
this.options.width = `${this.width}px`;
cleanupElements(this.DOMelements.lines);
cleanupElements(this.DOMelements.labels);
this.redraw();
resized = true;
}
else {
cleanupElements(this.DOMelements.lines);
cleanupElements(this.DOMelements.labels);
resized = false;
}
return resized;
}
/**
* converts value
* @param {number} value
* @returns {number} converted number
*/
convertValue(value) {
return this.scale.convertValue(value);
}
/**
* converts value
* @param {number} x
* @returns {number} screen value
*/
screenToValue(x) {
return this.scale.screenToValue(x);
}
/**
* Create a label for the axis at position x
*
* @param {number} y
* @param {string} text
* @param {'top'|'right'|'bottom'|'left'} orientation
* @param {string} className
* @param {number} characterHeight
* @private
*/
_redrawLabel(y, text, orientation, className, characterHeight) {
// reuse redundant label
const label = getDOMElement('div', this.DOMelements.labels, this.dom.frame); //this.dom.redundant.labels.shift();
label.className = className;
label.innerHTML = util$3.xss(text);
if (orientation === 'left') {
label.style.left = `-${this.options.labelOffsetX}px`;
label.style.textAlign = "right";
}
else {
label.style.right = `-${this.options.labelOffsetX}px`;
label.style.textAlign = "left";
}
label.style.top = `${y - 0.5 * characterHeight + this.options.labelOffsetY}px`;
text += '';
const largestWidth = Math.max(this.props.majorCharWidth, this.props.minorCharWidth);
if (this.maxLabelSize < text.length * largestWidth) {
this.maxLabelSize = text.length * largestWidth;
}
}
/**
* Create a minor line for the axis at position y
* @param {number} y
* @param {'top'|'right'|'bottom'|'left'} orientation
* @param {string} className
* @param {number} offset
* @param {number} width
*/
_redrawLine(y, orientation, className, offset, width) {
if (this.master === true) {
const line = getDOMElement('div', this.DOMelements.lines, this.dom.lineContainer); //this.dom.redundant.lines.shift();
line.className = className;
line.innerHTML = '';
if (orientation === 'left') {
line.style.left = `${this.width - offset}px`;
}
else {
line.style.right = `${this.width - offset}px`;
}
line.style.width = `${width}px`;
line.style.top = `${y}px`;
}
}
/**
* Create a title for the axis
* @private
* @param {'top'|'right'|'bottom'|'left'} orientation
*/
_redrawTitle(orientation) {
prepareElements(this.DOMelements.title);
// Check if the title is defined for this axes
if (this.options[orientation].title !== undefined && this.options[orientation].title.text !== undefined) {
const title = getDOMElement('div', this.DOMelements.title, this.dom.frame);
title.className = `vis-y-axis vis-title vis-${orientation}`;
title.innerHTML = util$3.xss(this.options[orientation].title.text);
// Add style - if provided
if (this.options[orientation].title.style !== undefined) {
util$3.addCssText(title, this.options[orientation].title.style);
}
if (orientation === 'left') {
title.style.left = `${this.props.titleCharHeight}px`;
}
else {
title.style.right = `${this.props.titleCharHeight}px`;
}
title.style.width = `${this.height}px`;
}
// we need to clean up in case we did not use all elements.
cleanupElements(this.DOMelements.title);
}
/**
* Determine the size of text on the axis (both major and minor axis).
* The size is calculated only once and then cached in this.props.
* @private
*/
_calculateCharSize() {
// determine the char width and height on the minor axis
if (!('minorCharHeight' in this.props)) {
const textMinor = document.createTextNode('0');
const measureCharMinor = document.createElement('div');
measureCharMinor.className = 'vis-y-axis vis-minor vis-measure';
measureCharMinor.appendChild(textMinor);
this.dom.frame.appendChild(measureCharMinor);
this.props.minorCharHeight = measureCharMinor.clientHeight;
this.props.minorCharWidth = measureCharMinor.clientWidth;
this.dom.frame.removeChild(measureCharMinor);
}
if (!('majorCharHeight' in this.props)) {
const textMajor = document.createTextNode('0');
const measureCharMajor = document.createElement('div');
measureCharMajor.className = 'vis-y-axis vis-major vis-measure';
measureCharMajor.appendChild(textMajor);
this.dom.frame.appendChild(measureCharMajor);
this.props.majorCharHeight = measureCharMajor.clientHeight;
this.props.majorCharWidth = measureCharMajor.clientWidth;
this.dom.frame.removeChild(measureCharMajor);
}
if (!('titleCharHeight' in this.props)) {
const textTitle = document.createTextNode('0');
const measureCharTitle = document.createElement('div');
measureCharTitle.className = 'vis-y-axis vis-title vis-measure';
measureCharTitle.appendChild(textTitle);
this.dom.frame.appendChild(measureCharTitle);
this.props.titleCharHeight = measureCharTitle.clientHeight;
this.props.titleCharWidth = measureCharTitle.clientWidth;
this.dom.frame.removeChild(measureCharTitle);
}
}
}
/**
*
* @param {number | string} groupId
* @param {Object} options // TODO: Describe options
*
* @constructor Points
*/
function Points(groupId, options) { // eslint-disable-line no-unused-vars
}
/**
* draw the data points
*
* @param {Array} dataset
* @param {GraphGroup} group
* @param {Object} framework | SVG DOM element
* @param {number} [offset]
*/
Points.draw = function (dataset, group, framework, offset) {
offset = offset || 0;
var callback = getCallback(framework, group);
for (var i = 0; i < dataset.length; i++) {
if (!callback) {
// draw the point the simple way.
drawPoint(dataset[i].screen_x + offset, dataset[i].screen_y, getGroupTemplate(group), framework.svgElements, framework.svg, dataset[i].label);
}
else {
var callbackResult = callback(dataset[i], group); // result might be true, false or an object
if (callbackResult === true || typeof callbackResult === 'object') {
drawPoint(dataset[i].screen_x + offset, dataset[i].screen_y, getGroupTemplate(group, callbackResult), framework.svgElements, framework.svg, dataset[i].label);
}
}
}
};
Points.drawIcon = function (group, x, y, iconWidth, iconHeight, framework) {
var fillHeight = iconHeight * 0.5;
var outline = getSVGElement("rect", framework.svgElements, framework.svg);
outline.setAttributeNS(null, "x", x);
outline.setAttributeNS(null, "y", y - fillHeight);
outline.setAttributeNS(null, "width", iconWidth);
outline.setAttributeNS(null, "height", 2 * fillHeight);
outline.setAttributeNS(null, "class", "vis-outline");
//Don't call callback on icon
drawPoint(x + 0.5 * iconWidth, y, getGroupTemplate(group), framework.svgElements, framework.svg);
};
/**
*
* @param {vis.Group} group
* @param {any} callbackResult
* @returns {{style: *, styles: (*|string), size: *, className: *}}
*/
function getGroupTemplate(group, callbackResult) {
callbackResult = (typeof callbackResult === 'undefined') ? {} : callbackResult;
return {
style: callbackResult.style || group.options.drawPoints.style,
styles: callbackResult.styles || group.options.drawPoints.styles,
size: callbackResult.size || group.options.drawPoints.size,
className: callbackResult.className || group.className
};
}
/**
*
* @param {Object} framework | SVG DOM element
* @param {vis.Group} group
* @returns {function}
*/
function getCallback(framework, group) {
var callback = undefined;
// check for the graph2d onRender
if (framework.options && framework.options.drawPoints && framework.options.drawPoints.onRender && typeof framework.options.drawPoints.onRender == 'function') {
callback = framework.options.drawPoints.onRender;
}
// override it with the group onRender if defined
if (group.group.options && group.group.options.drawPoints && group.group.options.drawPoints.onRender && typeof group.group.options.drawPoints.onRender == 'function') {
callback = group.group.options.drawPoints.onRender;
}
return callback;
}
/**
*
* @param {vis.GraphGroup.id} groupId
* @param {Object} options // TODO: Describe options
* @constructor Bargraph
*/
function Bargraph(groupId, options) { // eslint-disable-line no-unused-vars
}
Bargraph.drawIcon = function (group, x, y, iconWidth, iconHeight, framework) {
var fillHeight = iconHeight * 0.5;
var outline = getSVGElement("rect", framework.svgElements, framework.svg);
outline.setAttributeNS(null, "x", x);
outline.setAttributeNS(null, "y", y - fillHeight);
outline.setAttributeNS(null, "width", iconWidth);
outline.setAttributeNS(null, "height", 2 * fillHeight);
outline.setAttributeNS(null, "class", "vis-outline");
var barWidth = Math.round(0.3 * iconWidth);
var originalWidth = group.options.barChart.width;
var scale = originalWidth / barWidth;
var bar1Height = Math.round(0.4 * iconHeight);
var bar2Height = Math.round(0.75 * iconHeight);
var offset = Math.round((iconWidth - (2 * barWidth)) / 3);
drawBar(x + 0.5 * barWidth + offset, y + fillHeight - bar1Height - 1, barWidth, bar1Height, group.className + ' vis-bar', framework.svgElements, framework.svg, group.style);
drawBar(x + 1.5 * barWidth + offset + 2, y + fillHeight - bar2Height - 1, barWidth, bar2Height, group.className + ' vis-bar', framework.svgElements, framework.svg, group.style);
if (group.options.drawPoints.enabled == true) {
var groupTemplate = {
style: group.options.drawPoints.style,
styles: group.options.drawPoints.styles,
size: (group.options.drawPoints.size / scale),
className: group.className
};
drawPoint(x + 0.5 * barWidth + offset, y + fillHeight - bar1Height - 1, groupTemplate, framework.svgElements, framework.svg);
drawPoint(x + 1.5 * barWidth + offset + 2, y + fillHeight - bar2Height - 1, groupTemplate, framework.svgElements, framework.svg);
}
};
/**
* draw a bar graph
*
* @param {Array.} groupIds
* @param {Object} processedGroupData
* @param {{svg: Object, svgElements: Array.