Timeline performance improvements (#5669)

[ci skip]
pull/5681/head
Roman Roelofsen 7 years ago committed by Oliver Günther
parent 5db859ea39
commit 8d66228ec4
  1. 30
      frontend/app/components/wp-table/timeline/container/wp-timeline-container.directive.ts
  2. 66
      frontend/app/components/wp-table/timeline/grid/wp-timeline-grid.directive.ts
  3. 113
      frontend/app/components/wp-table/timeline/header/wp-timeline-header.directive.ts
  4. 66
      frontend/app/components/wp-table/timeline/wp-timeline.ts

@ -45,6 +45,7 @@ import {RenderedRow} from '../../../wp-fast-table/builders/primary-render-pass';
import {WorkPackageTimelineCellsRenderer} from '../cells/wp-timeline-cells-renderer';
import {WorkPackageTimelineCell} from '../cells/wp-timeline-cell';
import {WorkPackageRelationsService} from '../../../wp-relations/wp-relations.service';
import {Moment} from "moment";
export class WorkPackageTimelineTableController {
@ -63,6 +64,7 @@ export class WorkPackageTimelineTableController {
private cellsRenderer = new WorkPackageTimelineCellsRenderer(this);
public outerContainer:JQuery;
public timelineBody:JQuery;
private selectionParams = {
@ -157,6 +159,10 @@ export class WorkPackageTimelineTableController {
return this.$element.offset().left;
}
getParentScrollContainer() {
return this.outerContainer.closest(selectorTimelineSide)[0];
}
get viewParameters():TimelineViewParameters {
return this._viewParameters;
}
@ -191,7 +197,7 @@ export class WorkPackageTimelineTableController {
// Calculate overflowing width to set to outer container
// required to match width in all child divs
const currentWidth = this.outerContainer.closest(selectorTimelineSide)[0].scrollWidth;
const currentWidth = this.getParentScrollContainer().scrollWidth;
this.outerContainer.width(currentWidth);
});
}
@ -230,6 +236,22 @@ export class WorkPackageTimelineTableController {
});
}
getFirstDayInViewport() {
const outerContainer = this.getParentScrollContainer();
const scrollLeft = outerContainer.scrollLeft;
const nonVisibleDaysLeft = Math.floor(scrollLeft / this.viewParameters.pixelPerDay);
return this.viewParameters.dateDisplayStart.clone().add(nonVisibleDaysLeft, "days");
}
getLastDayInViewport() {
const outerContainer = this.getParentScrollContainer();
const scrollLeft = outerContainer.scrollLeft;
const width = outerContainer.offsetWidth;
const viewPortRight = scrollLeft + width;
const daysUntilViewPortEnds = Math.ceil(viewPortRight / this.viewParameters.pixelPerDay) + 1;
return this.viewParameters.dateDisplayStart.clone().add(daysUntilViewPortEnds, "days");
}
private resetSelectionMode() {
this._viewParameters.activeSelectionMode = null;
this._viewParameters.selectionModeStart = null;
@ -323,6 +345,12 @@ export class WorkPackageTimelineTableController {
this._viewParameters.dateDisplayEnd = newParams.dateDisplayEnd;
}
// Calculate the visible viewport
const firstDayInViewport = this.getFirstDayInViewport();
const lastDayInViewport = this.getLastDayInViewport();
const viewport: [Moment, Moment] = [firstDayInViewport, lastDayInViewport];
this._viewParameters.visibleViewportAtCalculationTime = viewport;
return changed;
}
}

@ -25,16 +25,18 @@
//
// See doc/COPYRIGHT.rdoc for more details.
// ++
import * as moment from 'moment';
import {openprojectModule} from '../../../../angular-modules';
import {TimelineZoomLevel} from '../../../api/api-v3/hal-resources/query-resource.service';
import {WorkPackageTimelineTableController} from '../container/wp-timeline-container.directive';
import {
TimelineViewParameters,
calculatePositionValueForDayCount,
getTimeSlicesForHeader,
timelineElementCssClass,
calculatePositionValueForDayCount, timelineGridElementCssClass
} from "../wp-timeline";
import {WorkPackageTimelineTableController} from "../container/wp-timeline-container.directive";
import * as moment from 'moment';
timelineGridElementCssClass,
TimelineViewParameters
} from '../wp-timeline';
import Moment = moment.Moment;
import {openprojectModule} from "../../../../angular-modules";
import {TimelineZoomLevel} from "../../../api/api-v3/hal-resources/query-resource.service";
export class WorkPackageTableTimelineGrid {
@ -80,43 +82,43 @@ export class WorkPackageTableTimelineGrid {
}
private renderLabelsDays(vp:TimelineViewParameters) {
this.renderTimeSlices(vp, "day", vp.dateDisplayStart, vp.dateDisplayEnd, (start, cell) => {
cell.style.paddingTop = "1px";
this.renderTimeSlices(vp, 'day', vp.dateDisplayStart, vp.dateDisplayEnd, (start, cell) => {
cell.style.paddingTop = '1px';
});
}
private renderLabelsWeeks(vp:TimelineViewParameters) {
this.renderTimeSlices(vp, "day", vp.dateDisplayStart, vp.dateDisplayEnd, (start, cell) => {
this.renderTimeSlices(vp, 'day', vp.dateDisplayStart, vp.dateDisplayEnd, (start, cell) => {
});
this.renderTimeSlices(vp, "week", vp.dateDisplayStart, vp.dateDisplayEnd, (start, cell) => {
this.renderTimeSlices(vp, 'week', vp.dateDisplayStart, vp.dateDisplayEnd, (start, cell) => {
cell.classList.add('-grid-highlight');
});
}
private renderLabelsMonths(vp:TimelineViewParameters) {
this.renderTimeSlices(vp, "week", vp.dateDisplayStart, vp.dateDisplayEnd, (start, cell) => {
this.renderTimeSlices(vp, 'week', vp.dateDisplayStart, vp.dateDisplayEnd, (start, cell) => {
});
this.renderTimeSlices(vp, "month", vp.dateDisplayStart, vp.dateDisplayEnd, (start, cell) => {
this.renderTimeSlices(vp, 'month', vp.dateDisplayStart, vp.dateDisplayEnd, (start, cell) => {
cell.classList.add('-grid-highlight');
});
}
private renderLabelsQuarters(vp:TimelineViewParameters) {
this.renderTimeSlices(vp, "month", vp.dateDisplayStart, vp.dateDisplayEnd, (start, cell) => {
this.renderTimeSlices(vp, 'month', vp.dateDisplayStart, vp.dateDisplayEnd, (start, cell) => {
});
this.renderTimeSlices(vp, "quarter", vp.dateDisplayStart, vp.dateDisplayEnd, (start, cell) => {
this.renderTimeSlices(vp, 'quarter', vp.dateDisplayStart, vp.dateDisplayEnd, (start, cell) => {
cell.classList.add('-grid-highlight');
});
}
private renderLabelsYears(vp:TimelineViewParameters) {
this.renderTimeSlices(vp, "month", vp.dateDisplayStart, vp.dateDisplayEnd, (start, cell) => {
this.renderTimeSlices(vp, 'month', vp.dateDisplayStart, vp.dateDisplayEnd, (start, cell) => {
});
this.renderTimeSlices(vp, "year", vp.dateDisplayStart, vp.dateDisplayEnd, (start, cell) => {
this.renderTimeSlices(vp, 'year', vp.dateDisplayStart, vp.dateDisplayEnd, (start, cell) => {
cell.classList.add('-grid-highlight');
});
}
@ -127,30 +129,30 @@ export class WorkPackageTableTimelineGrid {
endView:Moment,
cellCallback:(start:Moment, cell:HTMLElement) => void) {
const slices:[Moment, Moment][] = [];
const {inViewportAndBoundaries, rest} = getTimeSlicesForHeader(vp, unit, startView, endView);
const time = startView.clone().startOf(unit);
const end = endView.clone().endOf(unit);
while (time.isBefore(end)) {
const sliceStart = moment.max(time, startView).clone();
const sliceEnd = moment.min(time.clone().endOf(unit), endView).clone();
time.add(1, unit);
slices.push([sliceStart, sliceEnd]);
for (let [start, end] of inViewportAndBoundaries) {
const cell = document.createElement('div');
cell.classList.add(timelineElementCssClass, timelineGridElementCssClass);
cell.style.left = calculatePositionValueForDayCount(vp, start.diff(startView, 'days'));
cell.style.width = calculatePositionValueForDayCount(vp, end.diff(start, 'days') + 1);
this.gridContainer[0].appendChild(cell);
cellCallback(start, cell);
}
for (let [start, end] of slices) {
const cell = document.createElement("div");
setTimeout(() => {
for (let [start, end] of rest) {
const cell = document.createElement('div');
cell.classList.add(timelineElementCssClass, timelineGridElementCssClass);
cell.style.left = calculatePositionValueForDayCount(vp, start.diff(startView, "days"));
cell.style.width = calculatePositionValueForDayCount(vp, end.diff(start, "days") + 1);
cell.style.left = calculatePositionValueForDayCount(vp, start.diff(startView, 'days'));
cell.style.width = calculatePositionValueForDayCount(vp, end.diff(start, 'days') + 1);
this.gridContainer[0].appendChild(cell);
cellCallback(start, cell);
}
}, 0);
}
}
openprojectModule.component("wpTimelineGrid", {
openprojectModule.component('wpTimelineGrid', {
template: '<div class="wp-table-timeline--grid"></div>',
controller: WorkPackageTableTimelineGrid,
require: {

@ -1,3 +1,7 @@
import * as moment from 'moment';
import {openprojectModule} from '../../../../angular-modules';
import {TimelineZoomLevel} from '../../../api/api-v3/hal-resources/query-resource.service';
import {WorkPackageTimelineTableController} from '../container/wp-timeline-container.directive';
// -- copyright
// OpenProject is a project management system.
// Copyright (C) 2012-2015 the OpenProject Foundation (OPF)
@ -25,16 +29,8 @@
//
// See doc/COPYRIGHT.rdoc for more details.
// ++
import {
TimelineViewParameters,
timelineElementCssClass,
calculatePositionValueForDayCount
} from "../wp-timeline";
import {WorkPackageTimelineTableController} from "../container/wp-timeline-container.directive";
import * as moment from 'moment';
import {calculatePositionValueForDayCount, getTimeSlicesForHeader, TimelineViewParameters} from '../wp-timeline';
import Moment = moment.Moment;
import {openprojectModule} from "../../../../angular-modules";
import {TimelineZoomLevel} from "../../../api/api-v3/hal-resources/query-resource.service";
export const timelineHeaderCSSClass = 'wp-timeline--header-element';
@ -83,133 +79,132 @@ export class WorkPackageTimelineHeaderController {
}
private renderLabelsDays(vp:TimelineViewParameters) {
this.renderTimeSlices(vp, "month", 0, vp.dateDisplayStart, vp.dateDisplayEnd, (start, cell) => {
cell.innerHTML = start.format("MMM YYYY");
this.renderTimeSlices(vp, 'month', 0, vp.dateDisplayStart, vp.dateDisplayEnd, (start, cell) => {
cell.innerHTML = start.format('MMM YYYY');
cell.classList.add('wp-timeline--header-top-bold-element');
cell.style.height = "13px";
cell.style.height = '13px';
});
this.renderTimeSlices(vp, "week", 13, vp.dateDisplayStart, vp.dateDisplayEnd, (start, cell) => {
cell.innerHTML = start.format("ww");
this.renderTimeSlices(vp, 'week', 13, vp.dateDisplayStart, vp.dateDisplayEnd, (start, cell) => {
cell.innerHTML = start.format('ww');
cell.classList.add('-top-border');
cell.style.height = '32px';
});
this.renderTimeSlices(vp, "day", 23, vp.dateDisplayStart, vp.dateDisplayEnd, (start, cell) => {
cell.innerHTML = start.format("D");
this.renderTimeSlices(vp, 'day', 23, vp.dateDisplayStart, vp.dateDisplayEnd, (start, cell) => {
cell.innerHTML = start.format('D');
cell.classList.add('-top-border');
cell.style.height = '22px';
});
this.renderTimeSlices(vp, "day", 33, vp.dateDisplayStart, vp.dateDisplayEnd, (start, cell) => {
cell.innerHTML = start.format("dd");
this.renderTimeSlices(vp, 'day', 33, vp.dateDisplayStart, vp.dateDisplayEnd, (start, cell) => {
cell.innerHTML = start.format('dd');
cell.classList.add('wp-timeline--header-day-element');
});
}
private renderLabelsWeeks(vp:TimelineViewParameters) {
this.renderTimeSlices(vp, "month", 0, vp.dateDisplayStart, vp.dateDisplayEnd, (start, cell) => {
cell.innerHTML = start.format("MMM YYYY");
this.renderTimeSlices(vp, 'month', 0, vp.dateDisplayStart, vp.dateDisplayEnd, (start, cell) => {
cell.innerHTML = start.format('MMM YYYY');
cell.classList.add('wp-timeline--header-top-bold-element');
});
this.renderTimeSlices(vp, "week", 15, vp.dateDisplayStart, vp.dateDisplayEnd, (start, cell) => {
cell.innerHTML = start.format("ww");
this.renderTimeSlices(vp, 'week', 15, vp.dateDisplayStart, vp.dateDisplayEnd, (start, cell) => {
cell.innerHTML = start.format('ww');
cell.classList.add('-top-border');
cell.style.height = '22px';
});
this.renderTimeSlices(vp, "day", 25, vp.dateDisplayStart, vp.dateDisplayEnd, (start, cell) => {
cell.innerHTML = start.format("D");
this.renderTimeSlices(vp, 'day', 25, vp.dateDisplayStart, vp.dateDisplayEnd, (start, cell) => {
cell.innerHTML = start.format('D');
cell.classList.add('wp-timeline--header-middle-element');
});
}
private renderLabelsMonths(vp:TimelineViewParameters) {
this.renderTimeSlices(vp, "year", 0, vp.dateDisplayStart, vp.dateDisplayEnd, (start, cell) => {
cell.innerHTML = start.format("YYYY");
this.renderTimeSlices(vp, 'year', 0, vp.dateDisplayStart, vp.dateDisplayEnd, (start, cell) => {
cell.innerHTML = start.format('YYYY');
cell.classList.add('wp-timeline--header-top-bold-element');
});
this.renderTimeSlices(vp, "month", 15, vp.dateDisplayStart, vp.dateDisplayEnd, (start, cell) => {
cell.innerHTML = start.format("MMM");
this.renderTimeSlices(vp, 'month', 15, vp.dateDisplayStart, vp.dateDisplayEnd, (start, cell) => {
cell.innerHTML = start.format('MMM');
cell.classList.add('-top-border');
cell.style.height = '30px';
});
this.renderTimeSlices(vp, "week", 25, vp.dateDisplayStart, vp.dateDisplayEnd, (start, cell) => {
cell.innerHTML = start.format("ww");
this.renderTimeSlices(vp, 'week', 25, vp.dateDisplayStart, vp.dateDisplayEnd, (start, cell) => {
cell.innerHTML = start.format('ww');
cell.classList.add('wp-timeline--header-middle-element');
});
}
private renderLabelsQuarters(vp:TimelineViewParameters) {
this.renderTimeSlices(vp, "year", 0, vp.dateDisplayStart, vp.dateDisplayEnd, (start, cell) => {
this.renderTimeSlices(vp, 'year', 0, vp.dateDisplayStart, vp.dateDisplayEnd, (start, cell) => {
cell.classList.add('wp-timeline--header-top-bold-element');
cell.innerHTML = start.format("YYYY");
cell.innerHTML = start.format('YYYY');
});
this.renderTimeSlices(vp, "quarter", 15, vp.dateDisplayStart, vp.dateDisplayEnd, (start, cell) => {
cell.innerHTML = "Q" + start.format("Q");
this.renderTimeSlices(vp, 'quarter', 15, vp.dateDisplayStart, vp.dateDisplayEnd, (start, cell) => {
cell.innerHTML = 'Q' + start.format('Q');
cell.classList.add('-top-border');
cell.style.height = '30px';
});
this.renderTimeSlices(vp, "month", 25, vp.dateDisplayStart, vp.dateDisplayEnd, (start, cell) => {
cell.innerHTML = start.format("MMM");
this.renderTimeSlices(vp, 'month', 25, vp.dateDisplayStart, vp.dateDisplayEnd, (start, cell) => {
cell.innerHTML = start.format('MMM');
cell.classList.add('wp-timeline--header-middle-element');
});
}
private renderLabelsYears(vp:TimelineViewParameters) {
this.renderTimeSlices(vp, "year", 0, vp.dateDisplayStart, vp.dateDisplayEnd, (start, cell) => {
cell.innerHTML = start.format("YYYY");
this.renderTimeSlices(vp, 'year', 0, vp.dateDisplayStart, vp.dateDisplayEnd, (start, cell) => {
cell.innerHTML = start.format('YYYY');
cell.classList.add('wp-timeline--header-top-bold-element');
});
this.renderTimeSlices(vp, "quarter", 15, vp.dateDisplayStart, vp.dateDisplayEnd, (start, cell) => {
cell.innerHTML = "Q" + start.format("Q");
this.renderTimeSlices(vp, 'quarter', 15, vp.dateDisplayStart, vp.dateDisplayEnd, (start, cell) => {
cell.innerHTML = 'Q' + start.format('Q');
cell.classList.add('-top-border');
cell.style.height = '30px';
});
this.renderTimeSlices(vp, "month", 25, vp.dateDisplayStart, vp.dateDisplayEnd, (start, cell) => {
cell.innerHTML = start.format("M");
this.renderTimeSlices(vp, 'month', 25, vp.dateDisplayStart, vp.dateDisplayEnd, (start, cell) => {
cell.innerHTML = start.format('M');
cell.classList.add('wp-timeline--header-middle-element');
});
}
renderTimeSlices(vp:TimelineViewParameters,
private renderTimeSlices(vp:TimelineViewParameters,
unit:moment.unitOfTime.DurationConstructor,
marginTop:number,
startView:Moment,
endView:Moment,
cellCallback:(start:Moment, cell:HTMLElement) => void) {
const slices:[Moment, Moment][] = [];
const time = startView.clone().startOf(unit);
const end = endView.clone().endOf(unit);
const {inViewportAndBoundaries, rest} = getTimeSlicesForHeader(vp, unit, startView, endView);
while (time.isBefore(end)) {
const sliceStart = moment.max(time, startView).clone();
const sliceEnd = moment.min(time.clone().endOf(unit), endView).clone();
time.add(1, unit);
slices.push([sliceStart, sliceEnd]);
for (let [start, end] of inViewportAndBoundaries) {
const cell = this.addLabelCell();
cell.style.top = marginTop + 'px';
cell.style.left = calculatePositionValueForDayCount(vp, start.diff(startView, 'days'));
cell.style.width = calculatePositionValueForDayCount(vp, end.diff(start, 'days') + 1);
cellCallback(start, cell);
}
for (let [start, end] of slices) {
setTimeout(() => {
for (let [start, end] of rest) {
const cell = this.addLabelCell();
cell.style.top = marginTop + "px";
cell.style.left = calculatePositionValueForDayCount(vp, start.diff(startView, "days"));
cell.style.width = calculatePositionValueForDayCount(vp, end.diff(start, "days") + 1);
cell.style.top = marginTop + 'px';
cell.style.left = calculatePositionValueForDayCount(vp, start.diff(startView, 'days'));
cell.style.width = calculatePositionValueForDayCount(vp, end.diff(start, 'days') + 1);
cellCallback(start, cell);
}
}, 0);
}
private addLabelCell():HTMLElement {
const label = document.createElement("div");
const label = document.createElement('div');
label.className = timelineHeaderCSSClass;
this.innerHeader.append(label);
@ -217,7 +212,7 @@ export class WorkPackageTimelineHeaderController {
}
}
openprojectModule.component("wpTimelineHeader", {
openprojectModule.component('wpTimelineHeader', {
templateUrl: '/components/wp-table/timeline/header/wp-timeline-header.html',
controller: WorkPackageTimelineHeaderController,
require: {

@ -25,17 +25,17 @@
//
// See doc/COPYRIGHT.rdoc for more details.
// ++
import * as moment from "moment";
import * as moment from 'moment';
import {TimelineZoomLevel} from '../../api/api-v3/hal-resources/query-resource.service';
import {
WorkPackageResourceInterface,
WorkPackageResource
} from "../../api/api-v3/hal-resources/work-package-resource.service";
WorkPackageResource,
WorkPackageResourceInterface
} from '../../api/api-v3/hal-resources/work-package-resource.service';
import Moment = moment.Moment;
import {TimelineZoomLevel} from "../../api/api-v3/hal-resources/query-resource.service";
export const timelineElementCssClass = "timeline-element";
export const timelineGridElementCssClass = "wp-timeline--grid-element";
export const timelineMarkerSelectionStartClass = "selection-start";
export const timelineElementCssClass = 'timeline-element';
export const timelineGridElementCssClass = 'wp-timeline--grid-element';
export const timelineMarkerSelectionStartClass = 'selection-start';
/**
*
@ -61,7 +61,7 @@ export class TimelineViewParameters {
dateDisplayStart:Moment = moment({hour: 0, minute: 0, seconds: 0});
dateDisplayEnd: Moment = this.dateDisplayStart.clone().add(1, "day");
dateDisplayEnd:Moment = this.dateDisplayStart.clone().add(1, 'day');
settings:TimelineViewParametersSettings = new TimelineViewParametersSettings();
@ -69,6 +69,11 @@ export class TimelineViewParameters {
selectionModeStart:null | string = null;
/**
* The visible viewport (at the time the view parameters were calculated last!!!)
*/
visibleViewportAtCalculationTime:[Moment, Moment];
get pixelPerDay():number {
switch (this.settings.zoomLevel) {
case 'days':
@ -89,7 +94,7 @@ export class TimelineViewParameters {
}
get maxSteps():number {
return this.dateDisplayEnd.diff(this.dateDisplayStart, "days");
return this.dateDisplayEnd.diff(this.dateDisplayStart, 'days');
}
}
@ -115,7 +120,46 @@ export function calculatePositionValueForDayCountingPx(viewParams: TimelineViewP
*/
export function calculatePositionValueForDayCount(viewParams:TimelineViewParameters, days:number):string {
const value = calculatePositionValueForDayCountingPx(viewParams, days);
return value + "px";
return value + 'px';
}
export function getTimeSlicesForHeader(vp:TimelineViewParameters,
unit:moment.unitOfTime.DurationConstructor,
startView:Moment,
endView:Moment) {
const inViewport:[Moment, Moment][] = [];
const rest:[Moment, Moment][] = [];
const time = startView.clone().startOf(unit);
const end = endView.clone().endOf(unit);
while (time.isBefore(end)) {
const sliceStart = moment.max(time, startView).clone();
const sliceEnd = moment.min(time.clone().endOf(unit), endView).clone();
time.add(1, unit);
const viewport = vp.visibleViewportAtCalculationTime;
if ((sliceStart.isSameOrAfter(viewport[0]) && sliceStart.isSameOrBefore(viewport[1]))
|| (sliceEnd.isSameOrAfter(viewport[0]) && sliceEnd.isSameOrBefore(viewport[1]))) {
inViewport.push([sliceStart, sliceEnd]);
} else {
rest.push([sliceStart, sliceEnd]);
}
}
const firstRest:[Moment, Moment] = rest.splice(0, 1)[0];
const lastRest:[Moment, Moment] = rest.pop()!;
const inViewportAndBoundaries = _.concat(
[firstRest].filter(e => !_.isNil(e)),
inViewport,
[lastRest].filter(e => !_.isNil(e))
);
return {
inViewportAndBoundaries,
rest
};
}

Loading…
Cancel
Save