Skip to content

Commit

Permalink
Rescale timeline-chart post zoom using PIXI.Container scale property
Browse files Browse the repository at this point in the history
This commit uses PIXI.Container scale property to rescale the chart after
each zoom in/zoom out to instead of the using the manually repositioning
each elements in the chart to improve the performance of the timeline chart.
The scale of the time-graph-chart is changed everytime the view range
changes, and is reset once the server has replied to the front-end.

This commit removes the method updateScaleAndPosition(), which was used
to manually rescaling the chart.

Signed-off-by: Hoang Thuan Pham <hoang.pham@calian.ca>
  • Loading branch information
hoangphamEclipse committed May 24, 2023
1 parent 9e0527f commit 08fb2e6
Show file tree
Hide file tree
Showing 12 changed files with 235 additions and 137 deletions.
156 changes: 105 additions & 51 deletions doc/documentation.md
Original file line number Diff line number Diff line change
@@ -1,51 +1,105 @@
# General Scope
* The library is entirely client side, relying on the browser API.
* Time is measured using number, so any kind of x measure unit can be used.
* A function translating numbers into human readable form for labels and hovers can be provided by the library user.
* API for interaction with elements that will allow to register key and mouse listeners.
* The components are styleable. Those parts that are implemented in HTML allow CSS for styling. The timeline view\u2019s contents (gantt chart) provide an API for styling

# Components
## Time Controller
* A central time controller that manages time-related properties, like the total time range, viewed range, time cursor positions.
* It provides API for setting, reading and syncing the individual properties

## Time Cursor
* Time Cursor T1 and optionally T2 are displayed as a vertical line spanning all rows. Range between T1 and T2 will be highlighted. Styling is configurable through CSS.
* Time range selection (or range selection) has full read/write/listen API.
* Rendering is configurable through CSS and separated from the actual data.
Setting cursor T1 is doable by clicking anywhere in the main area. A mouse click while holding shift will set T2.
View Port positions (use to scroll and zoom) has full read/write/listen API.

## Time Axis
A reusable time axis component, that can be used independently of the other components. It syncs with a time controller and allows the user to change the controllers values (zooming, scrolling, setting cursors)
* The time axis is separated and usable alone, such that other widgets can render below and sync with the same time axis.
* It is styleable through CSS.
* A time axis controller is used to sync any number of widgets, so that scrolling in any of the timeline charts or the time axis will scroll all others accordingly.
* Clicking and dragging on the time axis will increase/decrease the zoom
* The view is connected to a time controller instance and sync its viewport, zoom level cursors bi-directionally

## Timeline View
* Shows data as a gantt chart on multiple rows.
* Optional labels can be shown in each state of each row.
* Optional marker symbols can be drawn over the states.
* An optional scrollbar on the bottom will allow to scroll on the x-axis.
* An optional scrollbar on the right will allow to scroll on the y-axis.
* Since implemented using [Pixi.js](https://www.pixijs.com/) (WebGL/Canvas) it will provide an API for styling.
* General possibilities to add additional graphical layers (e.g. markers or icons) is foreseen.
* The library provides with a pull hook, to lazily fetch data from any source.
* Selection of elements (single units) is accessible with a full read/write/listen API.
* Keystroke handling for navigation and time selection is supported. For a selected row, it's possible to use left/right arrow to go from one element's start time to end and vice versa, as well as skipping to the next/previous element on that row. Using shift + left mouse click will select a time range and the cursors T1 and T2 are drawn accordingly.
* WASD or IJKL keystrokes are available for zooming and navigation. The zooming is centered on the mouse cursor position.
* Horizontal zooming can be performed using Ctrl+mouse wheel. The zooming is centered on the mouse cursor position.
* Horizontal panning can be performed using the middle mouse button or Ctrl+left mouse button.
* Horizontal zooming selection can be performed using the right mouse button.
* Horizontal zooming can be cancelled by pressing the `esc` button while zooming using the right click + drag.
* The view is connected to a time controller instance and synchronizes its viewport, zoom level cursors bi-directionally.

## Data Model
* Library user can configure a data model provider which gets asked for data lazily depending on the viewport.
* Data is prefetched for x and y dimensions to make scrolling smoother.
* Data can be fetched for a given resolution, so the provider can optimize the amount of data provided.
* The data model provider allows to provide an array of rows, containing an array of elements. An element has a start time and a length. Arbitrary additional data can be provided, which is then used for styling and registered handlers. E.g., the hover provider would look up certain fields to display hover text.
* The data model provider allows to provide an array of arrows, pointing from a point to another point. Here a point is a coordinate in the timeline graph consisting of a time and row number. Arrows can have arbitrary additional properties used for styling and registered handlers.
# General Scope
* The library is entirely client side, relying on the browser API.
* Time is measured using number, so any kind of x measure unit can be used.
* A function translating numbers into human readable form for labels and hovers can be provided by the library user.
* API for interaction with elements that will allow to register key and mouse listeners.
* The components are styleable. Those parts that are implemented in HTML allow CSS for styling. The timeline view\u2019s contents (gantt chart) provide an API for styling

# Components
## Time Controller
* A central time controller that manages time-related properties, like the total time range, viewed range, time cursor positions.
* It provides API for setting, reading and syncing the individual properties

## Time Cursor
* Time Cursor T1 and optionally T2 are displayed as a vertical line spanning all rows. Range between T1 and T2 will be highlighted. Styling is configurable through CSS.
* Time range selection (or range selection) has full read/write/listen API.
* Rendering is configurable through CSS and separated from the actual data.
Setting cursor T1 is doable by clicking anywhere in the main area. A mouse click while holding shift will set T2.
View Port positions (use to scroll and zoom) has full read/write/listen API.

## Time Axis
A reusable time axis component, that can be used independently of the other components. It syncs with a time controller and allows the user to change the controllers values (zooming, scrolling, setting cursors)
* The time axis is separated and usable alone, such that other widgets can render below and sync with the same time axis.
* It is styleable through CSS.
* A time axis controller is used to sync any number of widgets, so that scrolling in any of the timeline charts or the time axis will scroll all others accordingly.
* Clicking and dragging on the time axis will increase/decrease the zoom
* The view is connected to a time controller instance and sync its viewport, zoom level cursors bi-directionally

## Timeline View
* Shows data as a gantt chart on multiple rows.
* Optional labels can be shown in each state of each row.
* Optional marker symbols can be drawn over the states.
* An optional scrollbar on the bottom will allow to scroll on the x-axis.
* An optional scrollbar on the right will allow to scroll on the y-axis.
* Since implemented using [Pixi.js](https://www.pixijs.com/) (WebGL/Canvas) it will provide an API for styling.
* General possibilities to add additional graphical layers (e.g. markers or icons) is foreseen.
* The library provides with a pull hook, to lazily fetch data from any source.
* Selection of elements (single units) is accessible with a full read/write/listen API.
* Keystroke handling for navigation and time selection is supported. For a selected row, it's possible to use left/right arrow to go from one element's start time to end and vice versa, as well as skipping to the next/previous element on that row. Using shift + left mouse click will select a time range and the cursors T1 and T2 are drawn accordingly.
* WASD or IJKL keystrokes are available for zooming and navigation. The zooming is centered on the mouse cursor position.
* Horizontal zooming can be performed using Ctrl+mouse wheel. The zooming is centered on the mouse cursor position.
* Horizontal panning can be performed using the middle mouse button or Ctrl+left mouse button.
* Horizontal zooming selection can be performed using the right mouse button.
* Horizontal zooming can be cancelled by pressing the `esc` button while zooming using the right click + drag.
* The view is connected to a time controller instance and synchronizes its viewport, zoom level cursors bi-directionally.

## Data Model
* Library user can configure a data model provider which gets asked for data lazily depending on the viewport.
* Data is prefetched for x and y dimensions to make scrolling smoother.
* Data can be fetched for a given resolution, so the provider can optimize the amount of data provided.
* The data model provider allows to provide an array of rows, containing an array of elements. An element has a start time and a length. Arbitrary additional data can be provided, which is then used for styling and registered handlers. E.g., the hover provider would look up certain fields to display hover text.
* The data model provider allows to provide an array of arrows, pointing from a point to another point. Here a point is a coordinate in the timeline graph consisting of a time and row number. Arrows can have arbitrary additional properties used for styling and registered handlers.

## Performance improvements

### Scaling

Scaling of the timeline chart is done using the `scale factor` property of the state controller.

* Any components that inherit from the `TimeGraphViewportLayer` can enable scaling using the `isScalable` property.
* The `TimeGraphViewportLayer` applies the `scale factor` value to the `PIXI.Container.scale.x` property to scale the chart.
* The `scale factor` is reset every time new data arrive from the server and the chart re-renders itself. A value of 1 means no scaling is applied.

There are 2 ways to calculate this factor, which will be described below.

#### On zoom

The timeline chart clears all `row components` and re-render the states every time new data is fetched from the server. While waiting for the new data to arrive from the server, the timeline chart re-scales the states to make it look like the chart was zoomed in or out instantly the moment the view range changes. This section describes how the scaling is implemented to support view range changes triggered by zooming.

When the view range is changed:

* The `updateScaleFactor` function of the `state controller` of the timeline chart are triggered. It re-calculates the `scale factor` of the timeline chart.
* Once the `scale factor` is updated, it will trigger the `onScaleFactorChange` event, upon which any components that are registered to this event can trigger their own handler.
* In case of `view range` changes, the `scale factor` is calculated as `newScaleFactor = (oldViewRangeLength / newViewRangeLength) * oldScaleFactor`. The multiplication with the old scale factor is to support multiple zooming while waiting for data.

Because the timeline chart requires the `oldViewRangeLength` to calculate the scale factor, the parameters of the `onViewRangeChanged` function has been updated:

```text
Previously: (viewRange)
New implementation: (oldRange, newRange)
With viewRange=newRange
```

**Important**: The `onViewRangeChanged()` handlers need to be removed when we destroy the timeline chart so that subsequent view range change event will not cause the error `cannot read property of undefined` (because all PIXI objects are destroyed).

#### On resize

When the timeline chart is resized, the `scale factor` is also recalculated:

```text
scaleFactor = canvasDisplayWidth / unscaledCanvasWidth;
where
canvasDisplayWidth = the current width of the canvas
unscaledCanvasWidth = the width of the canvas when no scaling is applied
```

The `unscaledCanvasWidth` is updated whenever the chart is reset.

#### Label text

When the timeline chart is scaled, the label text will also be scaled. This squishes the text when the chart shrinks, and stretches the text when the chart expands. Thus, the timeline chart rescales the texts using the function `scaleLabel()` of the `TimeGraphStateComponent` to re-render the labels with an appropriate scaling factor so that the text size stays consistent.

#### Note

* Some components, such as the selection cursors, do not need to be scaled when the timeline chart is zoomed/resized, but rather re-rendered. In the case of cursors, if scaling is applied, the timeline chart will display a thick vertical line, which is not the expected behavior.
* The `updateZoomingSelection()` function of the `TimeGraphChart` needs to undo the scaling to get the correct position of the user pointer.
2 changes: 1 addition & 1 deletion timeline-chart/src/components/time-graph-grid.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,6 @@ export class TimeGraphGrid extends TimeGraphAxisScale {
}

render(): void {
this.renderVerticalLines(false, this._options.lineColor || 0xdddddd, () => ({ lineHeight: this.stateController.canvasDisplayHeight }), true);
this.renderVerticalLines(false, this._options.lineColor || 0xdddddd, () => ({ lineHeight: this.stateController.canvasDisplayHeight }));
}
}
22 changes: 20 additions & 2 deletions timeline-chart/src/components/time-graph-state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ export interface TimeGraphStateStyle {
height?: number
borderWidth?: number
borderColor?: number
scale?: number
}

/**
Expand Down Expand Up @@ -71,7 +72,12 @@ export class TimeGraphStateComponent extends TimeGraphComponent<TimelineChart.Ti
};
}

renderLabel() {
/**
* Conveniently generate the labels of states and apply proper scaling when zooming
*
* @param scaleFactor
*/
renderLabel(scaleFactor: number = 1) {
if (!this.model.label) {
return;
}
Expand All @@ -92,7 +98,7 @@ export class TimeGraphStateComponent extends TimeGraphComponent<TimelineChart.Ti
if (fontStyle) {
const metrics = PIXI.TextMetrics.measureText(this.model.label, fontStyle);
// Round the text width up just to be sure that it will fit in the state
const textWidth = Math.ceil(metrics.width * SCALING_FACTOR);
const textWidth = Math.ceil(metrics.width * SCALING_FACTOR / scaleFactor);

let textObjX = position.x + textPadding;
const textObjY = position.y + textPadding;
Expand Down Expand Up @@ -126,6 +132,18 @@ export class TimeGraphStateComponent extends TimeGraphComponent<TimelineChart.Ti
this.textLabelObject.alpha = this._options.opacity ?? 1;
this.textLabelObject.x = textObjX;
this.textLabelObject.y = textObjY;
this.textLabelObject.scale.x = 1 / scaleFactor;
}
}

/**
* Scale only state labels that actually have text displayed.
*
* @param scaleFactor
*/
scaleLabel(scaleFactor?: number) {
if (this.textLabelObject && this.textLabelObject.scale && scaleFactor !== 1) {
this.renderLabel(scaleFactor);
}
}

Expand Down
7 changes: 7 additions & 0 deletions timeline-chart/src/layer/time-graph-chart-arrows.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { TimeGraphArrowComponent, TimeGraphArrowCoordinates } from "../components/time-graph-arrow";
import { TimeGraphElementPosition } from "../components/time-graph-component";
import { TimelineChart } from "../time-graph-model";
import { TimeGraphRowController } from "../time-graph-row-controller";
import { TimeGraphChartLayer } from "./time-graph-chart-layer";

export class TimeGraphChartArrows extends TimeGraphChartLayer {
Expand All @@ -9,9 +10,15 @@ export class TimeGraphChartArrows extends TimeGraphChartLayer {
protected rowIds: number[] = [];
private _updateHandler: { (): void; (worldRange: TimelineChart.TimeGraphRange): void; (worldRange: TimelineChart.TimeGraphRange): void; };

constructor(id: string, protected rowController: TimeGraphRowController) {
super(id, rowController);
this.isScalable = false;
}

protected afterAddToContainer() {
this._updateHandler = (): void => this.update();
this.stateController.onWorldRender(this._updateHandler);
this.stateController.onScaleFactorChange(this._updateHandler);

this.rowController.onVerticalOffsetChangedHandler(verticalOffset => {
this.layer.position.y = -verticalOffset;
Expand Down
6 changes: 6 additions & 0 deletions timeline-chart/src/layer/time-graph-chart-cursors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ export class TimeGraphChartCursors extends TimeGraphChartLayer {

constructor(id: string, protected chartLayer: TimeGraphChart, protected rowController: TimeGraphRowController, style?: { color?: number }) {
super(id, rowController);
this.isScalable = false;
if (style && style.color) {
this.color = style.color;
}
Expand Down Expand Up @@ -154,6 +155,7 @@ export class TimeGraphChartCursors extends TimeGraphChartLayer {
this.onCanvasEvent('mousedown', this._mouseDownHandler);
this.stateController.onWorldRender(this._updateHandler);
this.unitController.onSelectionRangeChange(this._updateHandler);
this.stateController.onScaleFactorChange(this._updateHandler);
this.update();
}

Expand Down Expand Up @@ -232,6 +234,10 @@ export class TimeGraphChartCursors extends TimeGraphChartLayer {

update() {
if (this.unitController.selectionRange) {
/**
* When user selects a range on the timeline chart, the selection position must correspond to the cursor of the user,
* and not the timeline chart itself since scaling might be applied.
*/
const firstCursorPosition = this.getWorldPixel(this.unitController.selectionRange.start);
const secondCursorPosition = this.getWorldPixel(this.unitController.selectionRange.end);
const firstCursorOptions = {
Expand Down
6 changes: 4 additions & 2 deletions timeline-chart/src/layer/time-graph-chart-grid.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { TimeGraphViewportLayer } from "./time-graph-viewport-layer";
import { TimeGraphGrid } from "../components/time-graph-grid";
import { TimelineChart } from "../time-graph-model";
import { TimeGraphAxisLayerOptions } from "./time-graph-axis";
import { TimeGraphLayer } from "./time-graph-layer";

export class TimeGraphChartGrid extends TimeGraphViewportLayer {
export class TimeGraphChartGrid extends TimeGraphLayer {

protected gridComponent: TimeGraphGrid;
private _updateHandler: { (): void; (viewRange: TimelineChart.TimeGraphRange): void; (viewRange: TimelineChart.TimeGraphRange): void; (selectionRange: TimelineChart.TimeGraphRange): void; };;
Expand All @@ -22,6 +22,7 @@ export class TimeGraphChartGrid extends TimeGraphViewportLayer {
this.addChild(this.gridComponent);
this._updateHandler = (): void => this.update();
this.stateController.onWorldRender(this._updateHandler);
this.unitController.onViewRangeChanged(this._updateHandler);
}

update(opts?: TimeGraphAxisLayerOptions) {
Expand All @@ -32,6 +33,7 @@ export class TimeGraphChartGrid extends TimeGraphViewportLayer {
if (this.unitController) {
this.stateController.removeWorldRenderHandler(this._updateHandler);
this.unitController.removeSelectionRangeChangedHandler(this._updateHandler);
this.unitController.removeViewRangeChangedHandler(this._updateHandler);
}
super.destroy();
}
Expand Down
Loading

0 comments on commit 08fb2e6

Please sign in to comment.