blob: 4e0aa10bf7b724a5cf34c2a552dba3fbb3409388 [file] [log] [blame]
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {OnInit, Input, Output, EventEmitter} from '@angular/core';
import * as d3 from 'd3';
import * as moment from 'moment-timezone';
import {AppSettingsService} from '@app/services/storage/app-settings.service';
import {ChartTimeGap, GraphScaleItem} from '@app/classes/graph';
import {ServiceInjector} from '@app/classes/service-injector';
import {GraphComponent} from '@app/classes/components/graph/graph.component';
export class TimeGraphComponent extends GraphComponent implements OnInit {
constructor() {
super();
this.appSettings = ServiceInjector.injector.get(AppSettingsService);
}
ngOnInit() {
this.appSettings.getParameter('timeZone').subscribe((value: string): void => {
this.timeZone = value;
this.createGraph();
});
}
@Input()
tickTimeFormat: string = 'MM/DD HH:mm';
@Input()
historyStartEndTimeFormat: string = 'dddd, MMMM DD, YYYY';
@Output()
selectArea: EventEmitter<number[]> = new EventEmitter();
readonly isTimeGraph: boolean = true;
readonly allowFractionalXTicks: boolean = false;
protected appSettings: AppSettingsService;
protected dragArea: d3.Selection<SVGGraphicsElement, undefined, SVGGraphicsElement, undefined>;
protected dragStartX: number;
protected minDragX: number;
protected maxDragX: number;
protected timeZone: string;
/**
* This property holds the data structure describing the gaps between the xAxis ticks.
* The unit property can be: second, minute, hour, day
* The value is the number of the given unit.
*/
protected chartTimeGap: ChartTimeGap | null;
/**
* This is the rectangle element to represent the unselected time range on the left side of the selected time range
*/
protected leftDragArea: d3.Selection<SVGGraphicsElement, undefined, SVGGraphicsElement, undefined>;
/**
* This is the rectangle element to represent the unselected time range on the right side of the selected time range
*/
protected rightDragArea: d3.Selection<SVGGraphicsElement, undefined, SVGGraphicsElement, undefined>;
/**
* This is a Date object holding the value of the first tick of the xAxis. It is a helper getter for the template.
*/
protected get firstDateTick(): Date | undefined {
const ticks = this.xScale && this.xScale.ticks();
return (ticks && ticks.length && ticks[0]) || undefined;
}
/**
* This is a Date object holding the value of the last tick of the xAxis. It is a helper getter for the template.
*/
protected get lastDateTick(): Date | undefined {
const ticks = this.xScale && this.xScale.ticks();
return (ticks && ticks.length && ticks[ticks.length - 1]) || undefined;
}
protected xAxisTickFormatter = (tick: Date): string => {
return moment(tick).tz(this.timeZone).format(this.tickTimeFormat);
};
protected setXScaleDomain(data: GraphScaleItem[]): void {
this.xScale.domain(d3.extent(data, item => item.tick)).nice();
}
/**
* The goal is to calculate the time gap between the given dates. It will return an object representing the unit and
* the value in the given unit. Eg.: {unit: 'minute', value: 5}
* @param {Date} startDate
* @param {Date} endDate
* @returns {ChartTimeGap}
*/
protected getTimeGap(startDate: Date, endDate: Date): ChartTimeGap {
const startDateMoment = moment(startDate);
const endDateMoment = moment(endDate);
const diffInWeek: number = endDateMoment.diff(startDateMoment, 'weeks');
const diffInDay: number = endDateMoment.diff(startDateMoment, 'days');
const diffInHour: number = endDateMoment.diff(startDateMoment, 'hours');
const diffInMin: number = endDateMoment.diff(startDateMoment, 'minutes');
const diffInSec: number = endDateMoment.diff(startDateMoment, 'seconds');
const value = diffInWeek >= 1 ? diffInWeek : (
diffInDay >= 1 ? diffInDay : (
diffInHour >= 1 ? diffInHour : (diffInMin >= 1 ? diffInMin : diffInSec)
)
);
const unit: string = diffInWeek >= 1 ? 'week' : (
diffInDay >= 1 ? `day` : (
diffInHour >= 1 ? `hour` : (diffInMin >= 1 ? `minute` : `second`)
)
);
const label = `histogram.gap.${unit}${value > 1 ? 's' : ''}`;
return {
value,
unit,
label
};
}
/**
* The goal is to have a simple function to set the time gap corresponding to the xScale ticks.
* It will reset the time gap if the xScale is not set or there are no ticks.
*/
protected setChartTimeGapByXScale(): void {
let ticks = this.xScale && this.xScale.ticks();
if (ticks && ticks.length) {
this.setChartTimeGap(ticks[0], ticks[1] || ticks[0]);
} else {
this.resetChartTimeGap();
}
}
/**
* Simply reset the time gap property to null.
*/
protected resetChartTimeGap(): void {
this.chartTimeGap = null;
}
/**
* The goal is to have a single point where we set the chartTimeGap property corresponding the given timerange.
* @param {Date} startDate
* @param {Date} endDate
*/
protected setChartTimeGap(startDate: Date, endDate: Date): void {
this.chartTimeGap = this.getTimeGap(startDate, endDate);
}
protected getTimeRangeByXRanges(startX: number, endX: number): [number, number] {
const xScaleInterval = this.xScale.domain().map((point: Date): number => point.valueOf());
const xScaleLength = xScaleInterval[1] - xScaleInterval[0];
const ratio = xScaleLength / this.width;
return [Math.round(xScaleInterval[0] + ratio * startX), Math.round(xScaleInterval[0] + ratio * endX)];
}
/**
* The goal is to create the two shadow rectangle beside the selected area. Actually we blurout the not selected
* timeranges
* @param {number} startX This is the starting position of the drag event withing the container
* @param {number} currentX This is the ending point of the drag within the container
*/
protected createInvertDragArea(startX: number, currentX: number): void {
const height: number = this.height + this.margin.top + this.margin.bottom;
this.leftDragArea = this.svg.insert('rect').attr('height', height).attr('class', 'unselected-drag-area');
this.rightDragArea = this.svg.insert('rect').attr('height', height).attr('class', 'unselected-drag-area');
this.setInvertDragArea(startX, currentX);
}
/**
* Set the position and the width of the blur/shadow rectangles of the unselected area(s).
* @param {number} startX The start point of the selected area.
* @param {number} currentX The end point of the selected area.
*/
protected setInvertDragArea(startX: number, currentX: number): void {
const left: number = Math.min(startX, currentX);
const right: number = Math.max(startX, currentX);
const rightAreaWidth: number = Math.max(0, this.width - right);
const leftAreaWidth: number = Math.max(0, left);
this.leftDragArea.attr('x', 0).attr('width', leftAreaWidth);
this.rightDragArea.attr('x', right).attr('width', rightAreaWidth);
}
/**
* The goal is to have a single point where we remove the rectangles of the blur/shadow, unselected time range(s)
*/
protected clearInvertDragArea(): void {
this.leftDragArea.remove();
this.rightDragArea.remove();
}
protected setDragBehavior(): void {
this.minDragX = this.margin.left;
this.maxDragX = this.graphContainer.clientWidth;
d3.selectAll(`svg#${this.svgId}`).call(d3.drag()
.on('start', (datum: undefined, index: number, containers: d3.ContainerElement[]): void => {
if (this.dragArea) {
this.dragArea.remove();
}
this.dragStartX = Math.max(0, this.getDragX(containers[0]) - this.margin.left);
this.dragArea = this.svg.insert('rect', ':first-child').attr('x', this.dragStartX).attr('y', 0).attr('width', 0)
.attr('height', this.height).attr('class', 'drag-area');
})
.on('drag', (datum: undefined, index: number, containers: d3.ContainerElement[]): void => {
const mousePos = this.getDragX(containers[0]);
const currentX = Math.max(mousePos, this.minDragX) - this.margin.left;
const startX = Math.min(currentX, this.dragStartX);
const currentWidth = Math.abs(currentX - this.dragStartX);
this.dragArea.attr('x', startX).attr('width', currentWidth);
let timeRange = this.getTimeRangeByXRanges(startX, startX + currentWidth);
this.setChartTimeGap(new Date(timeRange[0]), new Date(timeRange[1]));
})
.on('end', (): void => {
const dragAreaDetails = this.dragArea.node().getBBox();
const startX = Math.max(0, dragAreaDetails.x);
const endX = Math.min(this.width, dragAreaDetails.x + dragAreaDetails.width);
const dateRange: [number, number] = this.getTimeRangeByXRanges(startX, endX);
this.selectArea.emit(dateRange);
this.dragArea.remove();
this.setChartTimeGap(new Date(dateRange[0]), new Date(dateRange[1]));
})
);
d3.selectAll(`svg#${this.svgId} .value, svg#${this.svgId} .axis`).call(d3.drag().on('start', (): void => {
d3.event.sourceEvent.stopPropagation();
}));
}
protected getDragX(element: d3.ContainerElement): number {
return d3.mouse(element)[0];
}
}