UNPKG

@future-grid/fgp-graph

Version:

fgp-graph is a chart lib based on Dygraphs

1,067 lines (937 loc) 109 kB
import Dygraph from 'dygraphs'; import { DataRequestTarget, DomAttrs, GraphCollection, GraphSeries, ViewConfig } from '../metadata/configurations'; import moment from 'moment-timezone'; import { Synchronizer } from '../extras/synchronizer'; import { LoadingSpinner } from '../services/dataService'; import { GraphInteractions } from '../extras/interactions'; import { Formatters } from '../extras/formatters'; import { FgpColor, hsvToRGB } from '../services/colorService'; import FgpGraph from "../index"; import { EventHandlers } from "../metadata/graphoptions"; import Toolbar from "../extras/toolbar/Toolbar"; import RangeHandles from "../extras/RangeHandles"; import RectSelection from "../extras/toolbar/RectSelection"; export class DomElementOperator { static createElement = (type: string, attrs: Array<DomAttrs>): HTMLElement => { let dom: HTMLElement = document.createElement(type); let errorHappened = false; // put attributes on element attrs.forEach(attr => { // check the attribute, if exist then throw exception if (!dom.getAttribute(attr.key)) { dom.setAttribute(attr.key, attr.value); } else { throw new Error("Duplicate Attrs " + attr.key); } }); return dom; } } export class GraphOperator { public static FIELD_PATTERN = new RegExp(/data[.]{1}[a-zA-Z0-9]+/g); private graphId?: string; defaultGraphRanges: Array<{ name: string, value: number, show?: boolean }> = [ { name: "3 days", value: (1000 * 60 * 60 * 24 * 3), show: true }, { name: "7 days", value: 604800000, show: true }, { name: "1 month", value: 2592000000, show: false } ]; createElement = (type: string, attrs: Array<DomAttrs>): HTMLElement => { let dom: HTMLElement = document.createElement(type); // put attributes on element attrs.forEach(attr => { dom.setAttribute(attr.key, attr.value); }); return dom; }; private mainGraph: Dygraph; private rangebarGraph: Dygraph; private currentView!: ViewConfig; private currentCollection!: GraphCollection | undefined; private rangeCollection!: GraphCollection; private start!: number; private end!: number; public datewindowCallback: any; private currentDateWindow?: { start: number, end: number }; private currentGraphData: any[]; private readonly graphContainer: HTMLElement; private readonly graphBody: HTMLElement; private readonly spinner: LoadingSpinner; private xBoundary: [number, number]; private readonly yAxisBtnArea: HTMLElement; private readonly y2AxisBtnArea: HTMLElement; private lockedInterval: { name: string, interval: number } | undefined; private eventListeners?: EventHandlers; private readonly graphInstance: FgpGraph; private toolbar?: Toolbar; private rectSelection?: RectSelection; private colorLocked: boolean = false; private readonly needSync: boolean = false; private axesConfig?: any; private yScaleBtns: { 'left': HTMLElement | undefined, 'right': HTMLElement | undefined } = { 'left': undefined, 'right': undefined }; private yScaleLockStatus = { 'left': { lock: true, value: [NaN, NaN] }, 'right': { lock: true, value: [NaN, NaN] } }; constructor(mainGraph: Dygraph, rangeGraph: Dygraph, graphContainer: HTMLElement, graphBody: HTMLElement, datewindowCallback: any, fgpGraph: FgpGraph, eventListeners?: EventHandlers, id?: string, needSync: boolean = true) { this.mainGraph = mainGraph; this.graphId = id; this.graphInstance = fgpGraph; this.rangebarGraph = rangeGraph; this.graphContainer = graphContainer; this.datewindowCallback = datewindowCallback; this.graphBody = graphBody; this.eventListeners = eventListeners; this.currentGraphData = []; this.spinner = new LoadingSpinner(this.graphContainer); this.xBoundary = [0, 0]; let yAxisButtonAreaAttrs: Array<DomAttrs> = [{ key: 'class', value: 'fgp-graph-yaxis-btn-container' }]; this.yAxisBtnArea = DomElementOperator.createElement('div', yAxisButtonAreaAttrs); let y2AxisButtonAreaAttrs: Array<DomAttrs> = [{ key: 'class', value: 'fgp-graph-y2axis-btn-container' }]; this.y2AxisBtnArea = DomElementOperator.createElement('div', y2AxisButtonAreaAttrs); this.needSync = needSync } public recreateElement = (el: HTMLElement, withChildren: boolean) => { if (withChildren && el) { if (el.parentNode) { el.parentNode.replaceChild(el.cloneNode(true), el); } } else if (el && el.parentNode) { let newEl = el.cloneNode(false); while (el.hasChildNodes() && el.firstChild) newEl.appendChild(el.firstChild); el.parentNode.replaceChild(newEl, el); } }; public showSpinner = () => { if (this.spinner) { this.spinner.show(); } }; /** * call this method to highlight series */ public highlightSeries = (series: string[], duration: number, type?: string) => { let visibility: boolean[] = this.mainGraph.getOption("visibility"); let formatters: Formatters = new Formatters(this.currentView.timezone ? this.currentView.timezone : moment.tz.guess()); let ranges: Array<Array<number>> = this.mainGraph.yAxisRanges(); if (series && series.length > 0) { // check entities let seriesInGraph: Array<string> = []; series.forEach(_series => { // let existEntity = this.currentView.graphConfig.entities.find(entity => { return entity.id === _series; }); if (existEntity) { seriesInGraph.push(existEntity.name); } }); if (type && type === "selection" && seriesInGraph.length > 0) { // convert it to normal graph let graph: any = this.mainGraph; graph.setSelection(false, seriesInGraph[0]); } else if (seriesInGraph.length > 0) { // update "series" dropdown // hide all the others let _updateVisibility: boolean[] = []; if (this.currentCollection) { const _graphSeries = this.mainGraph.getLabels(); // get indexes let _indexsShow: number[] = []; _graphSeries.forEach((_series, _index) => { // if (_index != 0) { seriesInGraph.forEach((_showSeries, _showIndex) => { if (_showSeries === _series) { _indexsShow.push(_index - 1); } }); } }); if (_indexsShow.length === 0) { // not found // set visibility visibility.forEach((_v, _i) => { _updateVisibility.push(true); }); } else { // set visibility visibility.forEach((_v, _i) => { if (_indexsShow.indexOf(_i) == -1) { _v = false; } let exist: boolean = false; _indexsShow.forEach(_ei => { if (_ei === _i) { // found it exist = true; } }); if (!exist) { _updateVisibility.push(false); } else { _updateVisibility.push(true); } }); } this.mainGraph.updateOptions({ visibility: _updateVisibility, axes: { x: { axisLabelFormatter: formatters.axisLabel }, y: { valueRange: ranges[0], axisLabelWidth: 80 }, y2: ranges.length > 1 ? { valueRange: ranges[1], axisLabelWidth: 80 } : undefined } }); if (duration > 0) { // take all visibility back setTimeout(() => { _updateVisibility = []; visibility.forEach(_v => { _updateVisibility.push(true); }); this.mainGraph.updateOptions({ visibility: _updateVisibility, axes: { x: { axisLabelFormatter: formatters.axisLabel }, y: { valueRange: ranges[0], axisLabelWidth: 80 }, y2: ranges.length > 1 ? { valueRange: ranges[1], axisLabelWidth: 80 } : undefined } }); }, duration * 1000); } } } } else { // bring all back let _updateVisibility: Array<boolean> = []; visibility.forEach(_v => { _updateVisibility.push(true); }); this.mainGraph.updateOptions({ visibility: _updateVisibility, axes: { x: { axisLabelFormatter: formatters.axisLabel }, y: { valueRange: ranges[0], axisLabelWidth: 80 }, y2: ranges.length > 1 ? { valueRange: ranges[1], axisLabelWidth: 80 } : undefined } }); } }; init = (view: ViewConfig, readyCallback?: any, interactionCallback?: any) => { this.currentView = view; let formatters: Formatters = new Formatters(this.currentView.timezone ? this.currentView.timezone : moment.tz.guess()); let entities: Array<string> = []; let bottomAttrs: Array<DomAttrs> = [{ key: 'class', value: 'fgp-graph-bottom' }]; let bottom = null; this.currentView.graphConfig.entities.forEach(entity => { if (!entity.fragment) { entities.push(entity.id); } }); // bind rect selection this.rectSelection = new RectSelection(); if (this.rectSelection && this.currentView.interaction?.callback?.multiSelectionCallback) { this.rectSelection.setCallback((series: Array<string>) => { if (this.currentView.interaction && this.currentView.interaction.callback && this.currentView.interaction.callback.multiSelectionCallback) { let finalSeries: Array<string> = []; // find ids for series this.currentView.graphConfig.entities.forEach((entity) => { // let tempSeries = series.find((_name) => { return _name === entity.name; }); if (tempSeries) { finalSeries.push(entity.id); } }); this.currentView.interaction.callback.multiSelectionCallback(finalSeries); } }); } // find fields from configuration let timewindowEnd: number = moment.tz(this.currentView.timezone ? this.currentView.timezone : moment.tz.guess()).add(1, 'days').startOf('day').valueOf(); let timewindowStart: number = moment.tz(this.currentView.timezone ? this.currentView.timezone : moment.tz.guess()).subtract(7, 'days').startOf('day').valueOf(); // default 7 days const ranges: Array<{ name: string, value: number, show?: boolean }> | undefined = this.currentView.ranges; if (ranges && ranges.length > 0) { // get first "show" == true const selected = ranges.find((value: { name: string; value: number; show?: boolean }, index: number, arr: { name: string; value: number; show?: boolean }[]) => { return !!value.show; }); // not found then use first one if (!selected) { // just need to change start timewindowStart = moment.tz(this.currentView.timezone ? this.currentView.timezone : moment.tz.guess()).add(1, 'days').startOf('day').valueOf() - ranges[0].value; } else { timewindowStart = moment.tz(this.currentView.timezone ? this.currentView.timezone : moment.tz.guess()).add(1, 'days').startOf('day').valueOf() - selected.value; } } // set init range if (this.needSync && this.currentDateWindow) { timewindowEnd = this.currentDateWindow.end; timewindowStart = this.currentDateWindow.start; } else if (view.initRange) { timewindowEnd = moment(view.initRange.end).tz(this.currentView.timezone ? this.currentView.timezone : moment.tz.guess()).valueOf(); timewindowStart = moment(view.initRange.start).tz(this.currentView.timezone ? this.currentView.timezone : moment.tz.guess()).valueOf(); } // which one should be shown first? base on current window size? or base on the collection config? // get default time range from graph config let graphRangesConfig: Array<{ name: string, value: number, show?: boolean }> = []; if (this.currentView.ranges) { graphRangesConfig = this.currentView.ranges; } let dropdownOpts: Array<{ id: string, label: string, selected?: boolean }> = []; graphRangesConfig.forEach(config => { dropdownOpts.push( { id: config.name, label: config.name, selected: config.show } ); }); let choosedCollection: GraphCollection | undefined; // get fields let fieldsForCollection: any[] = []; // get range config and find the first and last this.currentView.graphConfig.rangeCollection.series.forEach(series => { let _tempFields: string[] | null = (series.exp).match(GraphOperator.FIELD_PATTERN); // replace all "data."" with "" if (_tempFields) { _tempFields = _tempFields.map(exp => exp.replace("data.", "")); } // put fields together fieldsForCollection = fieldsForCollection.concat(_tempFields); }); // tell outside highlight disappeared this.graphContainer.addEventListener("mouseleave", (e) => { if (this.currentView.interaction && this.currentView.interaction.callback && this.currentView.interaction.callback.highlightCallback) { this.currentView.interaction.callback.highlightCallback(0, null, []); this.graphInstance.children.forEach(child => { if (child.syncLegend) { child.graph.clearSelection(); } }); } }); // this.currentView.dataService.fetchFirstNLast([this.currentView.graphConfig.rangeEntity.id], this.currentView.graphConfig.rangeEntity.type, this.currentView.graphConfig.rangeCollection.name, Array.from(new Set(fieldsForCollection))).then(resp => { // get first and last records, just need start and end timestamp let first: any = { timestamp: moment.tz(this.currentView.timezone ? this.currentView.timezone : moment.tz.guess()).valueOf() }; let last: any = { timestamp: 0 }; // if init range exist put the second on here if (this.currentView.initRange && this.currentView.initRange.end) { last.timestamp = this.currentView.initRange.end; } // get all first and last then find out which first is the smalllest and last is the largest resp.forEach(entityData => { if (entityData.id == this.currentView.graphConfig.rangeEntity.id) { if (entityData.data && entityData.data.first && entityData.data.first.timestamp) { // if (first.timestamp > entityData.data.first.timestamp) { first = entityData.data.first; } } if (entityData.data && entityData.data.last && entityData.data.last.timestamp) { // if (last.timestamp < entityData.data.last.timestamp) { last = entityData.data.last; } } } }); // init empty graph with start and end no other data // let firstRanges: any = graphRangesConfig.find(range => range.show && range.show == true); let firstRanges: any = graphRangesConfig.find((range: { name: string; value: number; show?: boolean }, index: number, object: ({ name: string; value: number; show?: boolean })[]) => { return range ? range.show : false; }); if (!firstRanges) { // throw errors; console.warn("non default range for range-bar, use default 7 days"); firstRanges = { name: "7 days", value: 604800000, show: true }; } // get fields and labels this.currentView.graphConfig.collections.forEach(collection => { // if there is a config for what level need to show. if (collection.threshold && firstRanges.value) { // >= && < [ in the middle ) if (firstRanges.value > collection.threshold.min && firstRanges.value <= collection.threshold.max) { this.currentCollection = choosedCollection = collection; } } }); // get choose collection by width.... if (!choosedCollection && firstRanges) { // cal with width const width: number = this.graphContainer.offsetWidth; // const pointsCanBeShown: number = Math.round(width * .9); this.currentView.graphConfig.collections.forEach(collection => { // how many points in this interval if ((firstRanges.value / collection.interval) <= pointsCanBeShown) { if (!choosedCollection) { this.currentCollection = choosedCollection = collection; } else if (choosedCollection.interval > collection.interval) { this.currentCollection = choosedCollection = collection; } } }); } let initialData = [[first.timestamp], [last.timestamp]]; this.xBoundary = [first.timestamp, last.timestamp]; if (this.currentView.initRange) { if (this.currentView.initRange.start < first.timestamp) { initialData[0] = [this.currentView.initRange.start]; } if (this.currentView.initRange.end > last.timestamp) { initialData[1] = [this.currentView.initRange.end]; } // upate choosed collection const gap = this.currentView.initRange.end - this.currentView.initRange.start; choosedCollection = this.currentView.graphConfig.collections.find((collection: GraphCollection) => { return collection.threshold && (gap > collection.threshold.min && gap <= collection.threshold.max); }); } let isY2: boolean = false; let mainGraphLabels: Array<string> = []; // check visibility config let initVisibility: Array<boolean> = []; if (choosedCollection && this.currentView.graphConfig.entities.length == 1) { mainGraphLabels = []; choosedCollection.series.forEach((series, _index) => { mainGraphLabels.push(series.label); if (series.visibility == undefined || series.visibility) { initVisibility.push(true); } else if (!series.visibility) { initVisibility.push(false); } initialData.forEach((_data: any) => { _data[_index + 1] = null; }); if (series.yIndex == "right") { isY2 = true; } }); } else if (choosedCollection && this.currentView.graphConfig.entities.length > 1 && choosedCollection.series && choosedCollection.series[0]) { mainGraphLabels = []; entities.forEach((entity, _index) => { mainGraphLabels.push(entity); initialData.forEach((_data: any) => { _data[_index + 1] = null; }); }); } let yScale: any = null; let y2Scale: any = null; // check if there is a init scale if (choosedCollection && choosedCollection.initScales) { if (choosedCollection.initScales.left && (choosedCollection.initScales.left.min != 0 && choosedCollection.initScales.left.max != 0)) { yScale = { valueRange: [choosedCollection.initScales.left.min, choosedCollection.initScales.left.max] }; } if (choosedCollection.initScales.right && (choosedCollection.initScales.right.min != 0 && choosedCollection.initScales.right.max != 0)) { y2Scale = { valueRange: [choosedCollection.initScales.right.min, choosedCollection.initScales.right.max] }; } } // check if scale locked if (this.yScaleLockStatus.left.lock) { // set default range if ((isNaN(this.yScaleLockStatus.left.value[0]) || isNaN(this.yScaleLockStatus.left.value[1])) && yScale) { this.yScaleLockStatus.left.value = yScale; } else { yScale = { valueRange: [this.yScaleLockStatus.left.value] }; } } if (this.yScaleLockStatus.right.lock) { // set default rangesyncDateWindow if ((isNaN(this.yScaleLockStatus.right.value[0]) || isNaN(this.yScaleLockStatus.right.value[1])) && y2Scale) { this.yScaleLockStatus.right.value = y2Scale; } else { y2Scale = { valueRange: [this.yScaleLockStatus.right.value] }; } } if (choosedCollection) { // set currentCollection to this.currentCollection = choosedCollection; } let currentDatewindowOnMouseDown: any[] = []; const datewindowChangeFunc = (e: MouseEvent, yAxisRange?: Array<Array<number>>) => { let datewindow: number[] = []; if (this.rangebarGraph) { datewindow = this.rangebarGraph.xAxisRange(); } else { datewindow = this.mainGraph.xAxisRange(); } if (datewindow[0] == currentDatewindowOnMouseDown[0] && datewindow[1] == currentDatewindowOnMouseDown[1]) { // console.debug("no change!"); } else { // fetch data again // sorting this.currentView.graphConfig.collections.sort((a, b) => { return a.interval > b.interval ? 1 : -1; }); this.start = datewindow[0]; this.end = datewindow[1]; if (!this.lockedInterval) { choosedCollection = this.currentView.graphConfig.collections.find((collection) => { return collection.threshold && (datewindow[1] - datewindow[0]) <= (collection.threshold.max); }); } else if (this.currentCollection) { choosedCollection = this.currentCollection; if (this.currentView.graphConfig.features.pointLimits) { // check limit let gAreaW = this.mainGraph.getArea().w; let currentInterval = this.currentCollection.interval; let maxShowP = 0; if (gAreaW) { // call start and end maxShowP = gAreaW * currentInterval; } // get current datewindow if (this.start > (this.end - maxShowP)) { // go ahead } else { this.start = this.end - (maxShowP * 1.5); // update datewindow if (this.rangebarGraph) { this.rangebarGraph.updateOptions({ dateWindow: [this.start, this.end] }); } else { this.mainGraph.updateOptions({ dateWindow: [this.start, this.end] }); } } } } let collection: GraphCollection = { label: "", name: "", series: [], interval: 0 }; Object.assign(collection, choosedCollection); this.currentCollection = collection; this.rangeCollection = this.currentView.graphConfig.rangeCollection; this.update(undefined, undefined, true); } }; let updateTimer: number = 1; let callbackFuncForInteractions = (e: MouseEvent, yAxisRange: Array<Array<number>>, refreshData: any) => { if (refreshData) { datewindowChangeFunc(e, yAxisRange); } else { // set initsacle if (updateTimer) { window.clearTimeout(updateTimer); } if (yAxisRange) { yAxisRange.forEach((element, _index) => { if (_index == 0) { //left if (this.currentCollection && this.currentCollection.initScales && !this.currentCollection.initScales.left) { // do nothing here. } else if (this.currentCollection && this.yScaleLockStatus.left.lock) { this.yScaleLockStatus.left.value = element; // update graph yAxes if (this.axesConfig) { this.axesConfig.y.valueRange = element; updateTimer = window.setTimeout(() => { console.info("hello"); this.mainGraph.updateOptions({ axes: this.axesConfig }); }, 600); } } } else if (_index == 1) { if (this.currentCollection && this.currentCollection.initScales && !this.currentCollection.initScales.right) { // do nothing here. } else if (this.currentCollection && this.yScaleLockStatus.right.lock) { this.yScaleLockStatus.right.value = element; // update graph yAxes if (this.axesConfig) { this.axesConfig.y2.valueRange = element; updateTimer = window.setTimeout(() => { this.mainGraph.updateOptions({ axes: this.axesConfig }); }, 600); } } } }); } } if (interactionCallback) { // ready to update children interactionCallback(); } }; // create a interaction model instance let interactionModel: GraphInteractions = new GraphInteractions(callbackFuncForInteractions, [first.timestamp, last.timestamp]); let dateLabelLeftAttrs: Array<DomAttrs> = [{ key: 'class', value: 'fgp-graph-range-bar-date-label-left' }]; let startLabelLeft: HTMLElement = DomElementOperator.createElement('label', dateLabelLeftAttrs); let dateLabelRightAttrs: Array<DomAttrs> = [{ key: 'class', value: 'fgp-graph-range-bar-date-label-right' }]; let endLabelRight: HTMLElement = DomElementOperator.createElement('label', dateLabelRightAttrs); let currentSelection: any = null; let fullVisibility: Array<boolean> = []; mainGraphLabels.forEach(label => { fullVisibility.push(true); }); let interactionModelConfig: any = { 'mousedown': interactionModel.mouseDown, 'mouseup': interactionModel.mouseUp, 'mouseenter': interactionModel.mouseEnter, }; if (this.currentView.graphConfig.features.rangeLocked) { // remove all event listener. can't zooming, scrolling and panning } else { // disable scrolling. scrolling will change datetime window if (this.currentView.graphConfig.features.scroll) { interactionModelConfig["mousewheel"] = interactionModel.mouseScroll; interactionModelConfig["DOMMouseScroll"] = interactionModel.mouseScroll; interactionModelConfig["wheel"] = interactionModel.mouseScroll; } } if (this.currentView.graphConfig.features.zoom) { interactionModelConfig["mousemove"] = interactionModel.mouseMove; } // create toolbar on top, instead of old way! this.toolbar = new Toolbar(this.currentView, this.graphInstance.viewConfigs, (collections: GraphCollection[]) => { // udpate graph here console.log(`new collection config from badges!`, collections); const showCollection = collections.find(_coll => { return _coll.show; }); if (showCollection) { this.currentCollection = showCollection; if (showCollection.locked) { this.lockedInterval = { "name": showCollection.name, "interval": Number(showCollection.interval) }; } else { this.lockedInterval = undefined; } // reload data this.refresh(); } }, (collection, datewindow) => { this.currentCollection = collection; this.start = datewindow[0]; this.end = datewindow[1]; this.update(); if (this.rangebarGraph) { // shrink and grow base on middle datetime this.rangebarGraph.updateOptions({ dateWindow: [this.start, this.end] }); } if (interactionCallback) { interactionCallback(); } }, (view) => { // change show if (this.needSync && this.currentDateWindow) { view.initRange = this.currentDateWindow; } this.init(view, (graph: Dygraph) => { this.mainGraph = graph; this.graphInstance.children.forEach(graph => { // call updateDatewinow if (graph.id != this.graphInstance.id) { // update data graph.operator.refresh(); } }); // check if we need to tell others the view changed. if (this.graphInstance.eventListeners && this.graphInstance.eventListeners.onViewChange) { //f call this.graphInstance.eventListeners.onViewChange(this.graphInstance, view); } }, () => { this.graphInstance.children.forEach(graph => { // call updateDatewinow if (graph.id != this.graphInstance.id) { // update data graph.operator.refresh(); } }); }); }, (active: boolean) => { if (this.rectSelection && active) { this.rectSelection.enable(); } else { this.rectSelection?.disable(); } }, (isLocked) => { this.colorLocked = isLocked; }); let axes = this.axesConfig = { x: { axisLabelFormatter: formatters.axisLabel, ticker: formatters.DateTickerTZ }, y: yScale, y2: y2Scale }; // create graph instance this.mainGraph = new Dygraph(this.graphBody, initialData, { labels: ['x'].concat(mainGraphLabels), ylabel: choosedCollection && choosedCollection.yLabel ? choosedCollection.yLabel : "", y2label: choosedCollection && choosedCollection.y2Label ? choosedCollection.y2Label : "", rangeSelectorHeight: 30, visibility: initVisibility.length > 0 ? initVisibility : fullVisibility, legend: "follow", legendFormatter: this.currentView.graphConfig.features.legend ? this.currentView.graphConfig.features.legend : formatters.legendForSingleSeries, labelsKMB: choosedCollection?.yKMB == false ? false : true, // showLabelsOnHighlight: false, // drawAxesAtZero: true, connectSeparatedPoints: this.currentView.connectSeparatedPoints ? this.currentView.connectSeparatedPoints : false, axes, highlightSeriesBackgroundAlpha: this.currentView.highlightSeriesBackgroundAlpha ? this.currentView.highlightSeriesBackgroundAlpha : 0.5, highlightSeriesOpts: { strokeWidth: 1 }, highlightCallback: (e, x, ps, row, seriesName) => { // make sure we got current selection and even no highlightCall in viewConfig we still need to make click dbl working. currentSelection = seriesName; if (this.currentView.interaction && this.currentView.interaction.callback && this.currentView.interaction.callback.highlightCallback) { // find id in entities and send it back to outside const entity = this.currentView.graphConfig.entities.find((_entity) => { return _entity.name === currentSelection; }); if (entity) { this.currentView.interaction.callback.highlightCallback(x, entity.id, ps); } } this.graphInstance.children.forEach(child => { if (child.syncLegend) { child.graph.setSelection(row, seriesName); } }); }, unhighlightCallback: (e) => { currentSelection = null; }, clickCallback: (e, x, points) => { if (this.currentView.interaction && this.currentView.interaction.callback && this.currentView.interaction.callback.clickCallback) { const entity = this.currentView.graphConfig.entities.find((_entity) => { return _entity.name === currentSelection; }); if (entity) { this.currentView.interaction.callback.clickCallback(entity.id); } } }, interactionModel: interactionModelConfig, drawCallback: (g, is_initial) => { const xAxisRange: Array<number> = g.xAxisRange(); if (this.currentView.graphConfig.features.rangeBar && this.currentView.graphConfig.rangeCollection) { if (typeof (this.currentView.graphConfig.features.rangeBar) === "boolean") { startLabelLeft.innerHTML = moment.tz(xAxisRange[0], this.currentView.timezone ? this.currentView.timezone : moment.tz.guess()).format('lll z'); endLabelRight.innerHTML = moment.tz(xAxisRange[1], this.currentView.timezone ? this.currentView.timezone : moment.tz.guess()).format('lll z'); } else if (this.currentView.graphConfig.features.rangeBar.format) { const format: string = this.currentView.graphConfig.features.rangeBar.format; startLabelLeft.innerHTML = moment.tz(xAxisRange[0], this.currentView.timezone ? this.currentView.timezone : moment.tz.guess()).format(format); endLabelRight.innerHTML = moment.tz(xAxisRange[1], this.currentView.timezone ? this.currentView.timezone : moment.tz.guess()).format(format); } } if (this.spinner && this.spinner.isLoading) { // remove spinner from container this.spinner.done(); } if (this.toolbar) { this.toolbar.updateDateWindow(xAxisRange, this.xBoundary); } this.currentDateWindow = { start: xAxisRange[0], end: xAxisRange[1] }; // update datewindow this.datewindowCallback(xAxisRange, this.currentView); }, plugins: [this.rectSelection, this.toolbar] }); // add dbl event if (this.currentView && this.currentView.interaction && this.currentView.interaction.callback && this.currentView.interaction.callback.dbClickCallback) { const callbackFunc = this.currentView.interaction.callback.dbClickCallback; this.graphBody.addEventListener('dblclick', (e) => { if (currentSelection) { // find entity let entity = this.currentView.graphConfig.entities.find((entity) => { return entity.name === currentSelection; }); // if (entity) { callbackFunc(entity.id); } } }); } let ctrlBtnTimer: any = null; const ctrlBtnsEventListener: EventListener = (e) => { if (e.target instanceof Element) { const btn: Element = e.target; const g: Dygraph = this.mainGraph; let ranges: Array<Array<number>> = this.mainGraph.yAxisRanges(); if (g && btn.getAttribute("fgp-ctrl") === "x-pan-left") { let newDatewindow = [0, 0]; // move left // current datewindow const datewindow: [number, number] = g.xAxisRange(); const dateGap = datewindow[1] - datewindow[0]; // if (this.xBoundary[0] < (datewindow[0] - dateGap)) { // move allowed newDatewindow[0] = datewindow[0] - dateGap; newDatewindow[1] = datewindow[1] - dateGap; } else { newDatewindow[0] = this.xBoundary[0]; newDatewindow[1] = datewindow[1] - (datewindow[0] - this.xBoundary[0]); } // update datewindow this.mainGraph.updateOptions({ dateWindow: newDatewindow, axes: { x: { axisLabelFormatter: formatters.axisLabel }, y: { valueRange: ranges[0], axisLabelWidth: 80 }, y2: ranges.length > 1 ? { valueRange: ranges[1], axisLabelWidth: 80 } : undefined } }); // update graph if (ctrlBtnTimer) { window.clearTimeout(ctrlBtnTimer); } ctrlBtnTimer = window.setTimeout(() => { // how to updat this.refresh(); if (interactionCallback) { interactionCallback(); } }, 1000); } else if (g && btn.getAttribute("fgp-ctrl") === "x-pan-right") { let newDatewindow = [0, 0]; // move left // current datewindow const datewindow: [number, number] = g.xAxisRange(); const dateGap = datewindow[1] - datewindow[0]; // if (this.xBoundary[1] > (datewindow[1] + dateGap)) { // move allowed newDatewindow[0] = datewindow[0] + dateGap; newDatewindow[1] = datewindow[1] + dateGap; } else { newDatewindow[1] = this.xBoundary[1]; newDatewindow[0] = datewindow[0] + (this.xBoundary[1] - datewindow[1]); } // update datewindow this.mainGraph.updateOptions({ dateWindow: newDatewindow, axes: { x: { axisLabelFormatter: formatters.axisLabel }, y: { valueRange: ranges[0], axisLabelWidth: 80 }, y2: ranges.length > 1 ? { valueRange: ranges[1], axisLabelWidth: 80 } : undefined } }); // update graph if (ctrlBtnTimer) { window.clearTimeout(ctrlBtnTimer); } ctrlBtnTimer = window.setTimeout(() => { // update graph this.refresh(); if (interactionCallback) { interactionCallback(); } }, 1000); } else if (g && btn.getAttribute("fgp-ctrl") === "x-zoom-in") { let newDatewindow = [0, 0]; // minimum ? left - right > 5 minutes const datewindow: [number, number] = g.xAxisRange(); const delta: number = (datewindow[1] - datewindow[0]) / 20; if ((datewindow[1] - delta) > ((datewindow[0] + delta) + (1000 * 60 * 300))) { newDatewindow = [datewindow[0] + delta, datewindow[1] - delta]; this.mainGraph.updateOptions({ dateWindow: newDatewindow, axes: { x: { axisLabelFormatter: formatters.axisLabel }, y: { valueRange: ranges[0], axisLabelWidth: 80 }, y2: ranges.length > 1 ? { valueRange: ranges[1], axisLabelWidth: 80 } : undefined } }); // update graph if (ctrlBtnTimer) { window.clearTimeout(ctrlBtnTimer); } ctrlBtnTimer = window.setTimeout(() => { // update graph this.refresh(); if (interactionCallback) { interactionCallback(); } }, 1000); } } else if (g && btn.getAttribute("fgp-ctrl") === "x-zoom-out") { let newDatewindow = [0, 0]; // minimum ? left - right > 5 minutes const datewindow: [number, number] = g.xAxisRange(); const delta: number = (datewindow[1] - datewindow[0]) / 20; if ((datewindow[1] + delta) < this.xBoundary[1]) { newDatewindow[1] = datewindow[1] + delta; } else { newDatewindow[1] = this.xBoundary[1]; } if ((datewindow[0] - delta) > this.xBoundary[0]) { newDatewindow[0] = datewindow[0] - delta; } else { newDatewindow[0] = this.xBoundary[0]; } this.mainGraph.updateOptions({ dateWindow: newDatewindow, axes: { x: { axisLabelFormatter: formatters.axisLabel }, y: { valueRange: ranges[0], axisLabelWidth: 80 }, y2: ranges.length > 1 ? {