Skip to content

Commit

Permalink
Use BigInt for timestamps
Browse files Browse the repository at this point in the history
Timestamps and time durations are changed to be of type 'bigint' instead
of 'number'.

This is done to avoid loss of precision in JavaScript 'number' type for
values above Number.MAX_SAFE_INTEGER (2^53 - 1).

The class BIMath is added to provide some BigInt math utility functions.

The method TimeGraphLayer.getPixels() is renamed to getPixel() since it
returns only one value.

In the time graph axis, fractional scale steps 1.5 and 2.5 are removed
to avoid non-equidistant ticks when zoomed in to nanosecond level, due
to BigInt integer rounding.

The minimum view range is set to 2 ns, to be able to zoom out of it and
to always have at least one time graph axis tick fully visible.

Make the minimum pan offset 1 ns when using keyboard 'A' or 'D' or
Shift-mouse wheel, for when the requested pixel offset represents less
than a nanosecond.

Refactor Ctrl+mouse drag horizontal panning to use the original start
position since the relative offset at each mouse move event can be less
than a nanosecond, preventing the panning.

Signed-off-by: Patrick Tasse <patrick.tasse@ericsson.com>
  • Loading branch information
PatrickTasse committed Oct 5, 2021
1 parent bbc0569 commit ed4042e
Show file tree
Hide file tree
Showing 20 changed files with 225 additions and 205 deletions.
28 changes: 16 additions & 12 deletions example/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,18 +36,22 @@ container.style.width = styleConfig.mainWidth + "px";
const testDataProvider = new TestDataProvider(styleConfig.mainWidth);
let timeGraph = testDataProvider.getData({});
const unitController = new TimeGraphUnitController(timeGraph.totalLength);
unitController.numberTranslator = (theNumber: number) => {
const milli = Math.floor(theNumber / 1000000);
const micro = Math.floor((theNumber % 1000000) / 1000);
const nano = Math.floor((theNumber % 1000000) % 1000);
return milli + ':' + micro + ':' + nano;
unitController.numberTranslator = (theNumber: bigint) => {
let num = theNumber.toString();
if (num.length > 6) {
num = num.slice(0, -6) + ':' + num.slice(-6);
}
if (num.length > 3) {
num = num.slice(0, -3) + ':' + num.slice(-3);
}
return num;
};

const providers = {
dataProvider: (range: TimelineChart.TimeGraphRange, resolution: number) => {
const length = range.end - range.start;
const overlap = ((length * 20) - length) / 2;
const start = range.start - overlap > 0 ? range.start - overlap : 0;
const overlap = length * BigInt(10);
const start = range.start - overlap > BigInt(0) ? range.start - overlap : BigInt(0);
const end = range.end + overlap < unitController.absoluteRange ? range.end + overlap : unitController.absoluteRange;
const newRange: TimelineChart.TimeGraphRange = { start, end };
const newResolution: number = resolution * 0.1;
Expand Down Expand Up @@ -155,19 +159,19 @@ timeGraphChartContainer.addLayers([timeGraphChartGridLayer, timeGraphChart,

timeGraphChart.registerMouseInteractions({
click: el => {
console.log('click: ' + el.constructor.name + ' : ' + JSON.stringify(el.model));
console.log('click: ' + el.constructor.name, el.model);
},
mouseover: el => {
console.log('mouseover: ' + el.constructor.name + ' : ' + JSON.stringify(el.model));
console.log('mouseover: ' + el.constructor.name, el.model);
},
mouseout: el => {
console.log('mouseout: ' + el.constructor.name + ' : ' + JSON.stringify(el.model));
console.log('mouseout: ' + el.constructor.name, el.model);
},
mousedown: el => {
console.log('mousedown: ' + el.constructor.name + ' : ' + JSON.stringify(el.model));
console.log('mousedown: ' + el.constructor.name, el.model);
},
mouseup: el => {
console.log('mouseup: ' + el.constructor.name + ' : ' + JSON.stringify(el.model));
console.log('mouseup: ' + el.constructor.name, el.model);
}
});

Expand Down
8 changes: 4 additions & 4 deletions example/src/test-arrows.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,16 @@ export const timeGraphArrows: TimelineChart.TimeGraphArrow[] = [
sourceId: 1,
destinationId: 2,
range:{
start: 1332170682486039800,
end: 1332170682489988000
start: BigInt('1332170682486039800'),
end: BigInt('1332170682489988000')
}
},
{
sourceId: 2,
destinationId: 1,
range:{
start: 1332170682497734100,
end: 1332170682497814000
start: BigInt('1332170682497734100'),
end: BigInt('1332170682497814000')
}
}
]
46 changes: 23 additions & 23 deletions example/src/test-data-provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -150,30 +150,30 @@ export namespace TestData {
}

export class TestDataProvider {
protected absoluteStart: number;
protected totalLength: number;
protected absoluteStart: bigint;
protected totalLength: bigint;
protected timeGraphEntries: object[];
protected timeGraphRows: object[];
protected canvasDisplayWidth: number;

constructor(canvasDisplayWidth: number) {
this.timeGraphEntries = timeGraphEntries.model.entries;
this.timeGraphRows = timeGraphStates.model.rows;
this.totalLength = 0;
this.totalLength = BigInt(0);

this.canvasDisplayWidth = canvasDisplayWidth;

this.timeGraphEntries.forEach((entry: TestData.TimeGraphEntry, rowIndex: number) => {
const row = timeGraphStates.model.rows.find(row => row.entryID === entry.id);
if (!this.absoluteStart) {
this.absoluteStart = entry.startTime;
} else if (entry.startTime < this.absoluteStart) {
this.absoluteStart = entry.startTime;
this.absoluteStart = BigInt(entry.startTime);
} else if (BigInt(entry.startTime) < this.absoluteStart) {
this.absoluteStart = BigInt(entry.startTime);
}
if (row) {
row.states.forEach((state: TestData.TimeGraphState, stateIndex: number) => {
if (state.value > 0) {
const end = state.startTime + state.duration - entry.startTime;
const end = BigInt(state.startTime + state.duration - entry.startTime);
this.totalLength = end > this.totalLength ? end : this.totalLength;
}
});
Expand All @@ -183,21 +183,21 @@ export class TestDataProvider {

getData(opts: { range?: TimelineChart.TimeGraphRange, resolution?: number }): TimelineChart.TimeGraphModel {
const rows: TimelineChart.TimeGraphRowModel[] = [];
const range = opts.range || { start: 0, end: this.totalLength };
const resolution = opts.resolution || this.totalLength / this.canvasDisplayWidth;
const range = opts.range || { start: BigInt(0), end: this.totalLength };
const resolution = opts.resolution || Number(this.totalLength) / this.canvasDisplayWidth;
const commonRow = timeGraphStates.model.rows.find(row => row.entryId === -1);
const _rangeEvents = commonRow?.annotations;
const rangeEvents: TimelineChart.TimeGraphAnnotation[] = [];
const startTime = 1332170682440133097;
const startTime = BigInt(1332170682440133097);
_rangeEvents?.forEach((annotation: any, annotationIndex: number) => {
const start = annotation.range.start - startTime;
const start = BigInt(annotation.range.start) - startTime;
if (range.start < start && range.end > start) {
rangeEvents.push({
id: annotation.id,
category: annotation.category,
range: {
start: annotation.range.start - this.absoluteStart,
end: annotation.range.end - this.absoluteStart
start: BigInt(annotation.range.start) - this.absoluteStart,
end: BigInt(annotation.range.end) - this.absoluteStart
},
label: annotation.label,
data: annotation.data
Expand All @@ -210,14 +210,14 @@ export class TestDataProvider {
const annotations: TimelineChart.TimeGraphAnnotation[] = [];
const row = timeGraphStates.model.rows.find(row => row.entryID === entry.id);
let hasStates = false;
let prevPossibleState = 0;
let prevPossibleState = BigInt(0);
let nextPossibleState = this.totalLength;
if (row) {
hasStates = !!row.states.length;
row.states.forEach((state: any, stateIndex: number) => {
if (state.value > 0 && state.duration * (1 / resolution) > 1) {
const start = state.startTime - entry.startTime;
const end = state.startTime + state.duration - entry.startTime;
const start = BigInt(state.startTime - entry.startTime);
const end = BigInt(state.startTime + state.duration - entry.startTime);
if (end > range.start && start < range.end) {
states.push({
id: 'el_' + rowIndex + '_' + stateIndex,
Expand All @@ -228,24 +228,24 @@ export class TestDataProvider {
}
}
if (stateIndex === 0) {
prevPossibleState = state.startTime - entry.startTime;
prevPossibleState = BigInt(state.startTime - entry.startTime);
}
if (stateIndex === row.states.length - 1) {
nextPossibleState = state.startTime + state.duration - entry.startTime;
nextPossibleState = BigInt(state.startTime + state.duration - entry.startTime);
}
});

const _annotations = row.annotations;
if (!!_annotations) {
_annotations.forEach((annotation: any, annotationIndex: number) => {
const start = annotation.range.start - entry.startTime;
const start = BigInt(annotation.range.start - entry.startTime);
if (range.start < start && range.end > start) {
annotations.push({
id: annotation.id,
category: annotation.category,
range: {
start: annotation.range.start - this.absoluteStart,
end: annotation.range.end - this.absoluteStart
start: BigInt(annotation.range.start) - this.absoluteStart,
end: BigInt(annotation.range.end) - this.absoluteStart
},
label: annotation.label,
data: annotation.data
Expand All @@ -259,8 +259,8 @@ export class TestDataProvider {
id: entry.id,
name: entry.name[0],
range: {
start: 0,
end: entry.endTime - entry.startTime
start: BigInt(0),
end: BigInt(entry.endTime - entry.startTime)
},
states,
annotations,
Expand Down
3 changes: 2 additions & 1 deletion example/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@
"allowJs": true,
"lib": [
"es6",
"dom"
"dom",
"esnext.bigint"
],
"sourceMap": true,
"outDir": "./lib"
Expand Down
33 changes: 33 additions & 0 deletions timeline-chart/src/bigint-utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
export class BIMath {

static readonly round = (val: bigint | number): bigint => {
return typeof val === 'bigint' ? val : BigInt(Math.round(val));
};

static readonly clamp = (val: bigint | number, min: bigint, max: bigint): bigint => {
val = BIMath.round(val);
if (val < min) {
return min;
} else if (val > max) {
return max;
}
return val;
};

static readonly min = (val1: bigint | number, val2: bigint | number): bigint => {
val1 = BIMath.round(val1);
val2 = BIMath.round(val2);
return val1 <= val2 ? val1 : val2;
};

static readonly max = (val1: bigint | number, val2: bigint | number): bigint => {
val1 = BIMath.round(val1);
val2 = BIMath.round(val2);
return val1 >= val2 ? val1 : val2;
};

static readonly abs = (val: bigint | number): bigint => {
val = BIMath.round(val);
return val >= 0 ? val : -val;
};
};
66 changes: 17 additions & 49 deletions timeline-chart/src/components/time-graph-axis-scale.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { TimelineChart } from "../time-graph-model";
export interface TimeGraphAxisStyle extends TimeGraphStyledRect {
lineColor?: number
}
import { BIMath } from "../bigint-utils";

export class TimeGraphAxisScale extends TimeGraphComponent<null> {

Expand All @@ -30,18 +31,7 @@ export class TimeGraphAxisScale extends TimeGraphComponent<null> {
protected addEvents() {
const mouseMove = _.throttle(event => {
if (this.mouseIsDown) {
/**
Zoom around MousePosition on drag up/down
left here as an additional option
to be added later
*/
// const delta = event.data.global.y - this.mouseStartY;
// const zoomStep = (delta / 100);
// this.zoomAroundMousePointerOnDrag(zoomStep);

const delta = event.data.global.x - this.mouseStartX;
const zoomStep = (delta / 100);
this.zoomAroundLeftViewBorder(zoomStep);
this.zoomAroundLeftViewBorder(event.data.global.x);
}
}, 40);
this.addEvent('mousedown', event => {
Expand All @@ -63,14 +53,14 @@ export class TimeGraphAxisScale extends TimeGraphComponent<null> {
const minCanvasStepWidth = Math.max(labelWidth, 80);
const viewRangeLength = this.unitController.viewRangeLength;
const maxSteps = canvasDisplayWidth / minCanvasStepWidth;
const realStepLength = viewRangeLength / maxSteps;
const realStepLength = Number(viewRangeLength) / maxSteps;
const log = Math.log10(realStepLength);
let logRounded = Math.round(log);
const normalizedStepLength = Math.pow(10, logRounded);
const residual = realStepLength / normalizedStepLength;
const steps = this.unitController.scaleSteps || [1, 1.5, 2, 2.5, 5, 10];
const steps = this.unitController.scaleSteps || [1, 2, 5, 10];
const normStepLength = steps.find(s => s > residual);
const stepLength = normalizedStepLength * (normStepLength || 1);
const stepLength = Math.max(normalizedStepLength * (normStepLength || 1), 1);
return stepLength;
}

Expand All @@ -89,19 +79,19 @@ export class TimeGraphAxisScale extends TimeGraphComponent<null> {
const canvasDisplayWidth = this.stateController.canvasDisplayWidth;
const zoomFactor = this.stateController.zoomFactor;
const viewRangeStart = this.unitController.viewRange.start;
const iLo: number = Math.floor(viewRangeStart / stepLength);
const iHi: number = Math.ceil((canvasDisplayWidth / zoomFactor + viewRangeStart) / stepLength);
const iLo: number = Math.floor(Number(viewRangeStart) / stepLength);
const iHi: number = Math.ceil((canvasDisplayWidth / zoomFactor + Number(viewRangeStart)) / stepLength);
for (let i = iLo; i < iHi; i++) {
const absolutePosition = stepLength * i;
const xpos = (absolutePosition - viewRangeStart) * zoomFactor;
const time = BIMath.round(stepLength * i);
const xpos = Number(time - viewRangeStart) * zoomFactor;
if (xpos >= 0 && xpos < canvasDisplayWidth) {
const position = {
x: xpos,
y: this._options.position.y
};
let label;
if (drawLabels && this.unitController.numberTranslator) {
label = this.unitController.numberTranslator(absolutePosition);
label = this.unitController.numberTranslator(time);
if (label) {
const text = new PIXI.Text(label, {
fontSize: 10,
Expand Down Expand Up @@ -133,40 +123,18 @@ export class TimeGraphAxisScale extends TimeGraphComponent<null> {
this.renderVerticalLines(true, this._options.lineColor || 0x000000, (l) => ({ lineHeight: l === '' || l === undefined ? 5 : 10 }));
}

zoomAroundLeftViewBorder(zoomStep: number) {
const oldViewRangeLength = this.oldViewRange.end - this.oldViewRange.start;
const newViewRangeLength = oldViewRangeLength / (1 + (zoomStep));
let start = this.oldViewRange.start;
let end = start + newViewRangeLength;
if (end > this.unitController.absoluteRange) {
end = this.unitController.absoluteRange;
zoomAroundLeftViewBorder(mouseX: number) {
if (mouseX <= 0) {
return;
}
if (Math.trunc(start) !== Math.trunc(end)) {
const start = this.oldViewRange.start;
const end = BIMath.min(this.oldViewRange.start + BIMath.round(Number(this.oldViewRange.end - this.oldViewRange.start) * (this.mouseStartX / mouseX)),
this.unitController.absoluteRange);
if (BIMath.abs(end - start) > 1) {
this.unitController.viewRange = {
start,
end
}
}
}

zoomAroundMousePointerOnDrag(zoomStep: number) {
const oldViewRangeLength = this.oldViewRange.end - this.oldViewRange.start;
const newViewRangeLength = oldViewRangeLength / (1 + (zoomStep));
const normZoomFactor = newViewRangeLength / oldViewRangeLength;
const shiftedMouseX = normZoomFactor * this.mouseStartX;
const xOffset = this.mouseStartX - shiftedMouseX;
const viewRangeOffset = xOffset / (this.stateController.canvasDisplayWidth / oldViewRangeLength);
let start = this.oldViewRange.start + viewRangeOffset;
if (start < 0) {
start = 0;
}
let end = start + newViewRangeLength;
if (end > this.unitController.absoluteRange) {
end = this.unitController.absoluteRange;
}
this.unitController.viewRange = {
start,
end
}
}
}
7 changes: 4 additions & 3 deletions timeline-chart/src/components/time-graph-state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,8 @@ export class TimeGraphStateComponent extends TimeGraphComponent<TimelineChart.Ti
constructor(
id: string,
model: TimelineChart.TimeGraphState,
protected range: TimelineChart.TimeGraphRange,
xStart: number,
xEnd: number,
protected _row: TimeGraphRow,
protected _style: TimeGraphStateStyle = { color: 0xfffa66, height: 14 },
protected displayWidth: number,
Expand All @@ -33,11 +34,11 @@ export class TimeGraphStateComponent extends TimeGraphComponent<TimelineChart.Ti
this._height = _style.height || 14;
this._height = _row.height === 0 ? 0 : Math.min(this._height, _row.height - 1);
this._position = {
x: this.range.start,
x: xStart,
y: this._row.position.y + ((this.row.height - this._height) / 2)
};
// min width of a state should never be less than 1 (for visibility)
const width = Math.max(1, this.range.end - this.range.start);
const width = Math.max(1, xEnd - xStart);
this._options = {
color: _style.color,
opacity: _style.opacity,
Expand Down
4 changes: 2 additions & 2 deletions timeline-chart/src/layer/time-graph-axis-cursors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,8 @@ export class TimeGraphAxisCursors extends TimeGraphLayer {

update(): void {
if (this.unitController.selectionRange) {
const firstCursorPosition = this.getPixels(this.unitController.selectionRange.start - this.unitController.viewRange.start);
const secondCursorPosition = this.getPixels(this.unitController.selectionRange.end - this.unitController.viewRange.start);
const firstCursorPosition = this.getPixel(this.unitController.selectionRange.start - this.unitController.viewRange.start);
const secondCursorPosition = this.getPixel(this.unitController.selectionRange.end - this.unitController.viewRange.start);
const firstOpts = {
color: this.color,
position: {
Expand Down
Loading

0 comments on commit ed4042e

Please sign in to comment.