@future-grid/fgp-graph
Version:
fgp-graph is a chart lib based on Dygraphs
1,067 lines (937 loc) • 109 kB
text/typescript
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 ? {