From ed4042ecc82a608c6dec07596afc0479f0f869e4 Mon Sep 17 00:00:00 2001 From: Patrick Tasse Date: Thu, 9 Sep 2021 11:18:36 -0400 Subject: [PATCH] Use BigInt for timestamps 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 --- example/src/index.ts | 28 ++--- example/src/test-arrows.ts | 8 +- example/src/test-data-provider.ts | 46 ++++---- example/tsconfig.json | 3 +- timeline-chart/src/bigint-utils.ts | 33 ++++++ .../src/components/time-graph-axis-scale.ts | 66 +++--------- .../src/components/time-graph-state.ts | 7 +- .../src/layer/time-graph-axis-cursors.ts | 4 +- timeline-chart/src/layer/time-graph-axis.ts | 17 +-- .../src/layer/time-graph-chart-arrows.ts | 4 +- .../src/layer/time-graph-chart-cursors.ts | 45 ++++---- .../layer/time-graph-chart-selection-range.ts | 8 +- timeline-chart/src/layer/time-graph-chart.ts | 100 ++++++++++-------- timeline-chart/src/layer/time-graph-layer.ts | 7 +- .../src/layer/time-graph-navigator.ts | 19 ++-- .../layer/time-graph-range-events-layer.ts | 8 +- timeline-chart/src/time-graph-model.ts | 10 +- .../src/time-graph-state-controller.ts | 4 +- .../src/time-graph-unit-controller.ts | 10 +- timeline-chart/tsconfig.json | 3 +- 20 files changed, 225 insertions(+), 205 deletions(-) create mode 100644 timeline-chart/src/bigint-utils.ts diff --git a/example/src/index.ts b/example/src/index.ts index 347edd0..321690c 100644 --- a/example/src/index.ts +++ b/example/src/index.ts @@ -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; @@ -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); } }); diff --git a/example/src/test-arrows.ts b/example/src/test-arrows.ts index 3c61b57..3024a7d 100644 --- a/example/src/test-arrows.ts +++ b/example/src/test-arrows.ts @@ -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') } } ] \ No newline at end of file diff --git a/example/src/test-data-provider.ts b/example/src/test-data-provider.ts index 00bb467..485abdc 100644 --- a/example/src/test-data-provider.ts +++ b/example/src/test-data-provider.ts @@ -150,8 +150,8 @@ 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; @@ -159,21 +159,21 @@ export class TestDataProvider { 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; } }); @@ -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 @@ -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, @@ -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 @@ -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, diff --git a/example/tsconfig.json b/example/tsconfig.json index ec415f5..f5c9278 100644 --- a/example/tsconfig.json +++ b/example/tsconfig.json @@ -16,7 +16,8 @@ "allowJs": true, "lib": [ "es6", - "dom" + "dom", + "esnext.bigint" ], "sourceMap": true, "outDir": "./lib" diff --git a/timeline-chart/src/bigint-utils.ts b/timeline-chart/src/bigint-utils.ts new file mode 100644 index 0000000..4a81186 --- /dev/null +++ b/timeline-chart/src/bigint-utils.ts @@ -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; + }; +}; diff --git a/timeline-chart/src/components/time-graph-axis-scale.ts b/timeline-chart/src/components/time-graph-axis-scale.ts index b34a631..db54307 100644 --- a/timeline-chart/src/components/time-graph-axis-scale.ts +++ b/timeline-chart/src/components/time-graph-axis-scale.ts @@ -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 { @@ -30,18 +31,7 @@ export class TimeGraphAxisScale extends TimeGraphComponent { 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 => { @@ -63,14 +53,14 @@ export class TimeGraphAxisScale extends TimeGraphComponent { 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; } @@ -89,11 +79,11 @@ export class TimeGraphAxisScale extends TimeGraphComponent { 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, @@ -101,7 +91,7 @@ export class TimeGraphAxisScale extends TimeGraphComponent { }; 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, @@ -133,40 +123,18 @@ export class TimeGraphAxisScale extends TimeGraphComponent { 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 - } - } } diff --git a/timeline-chart/src/components/time-graph-state.ts b/timeline-chart/src/components/time-graph-state.ts index c1d8c19..49e4cc2 100644 --- a/timeline-chart/src/components/time-graph-state.ts +++ b/timeline-chart/src/components/time-graph-state.ts @@ -23,7 +23,8 @@ export class TimeGraphStateComponent extends TimeGraphComponent { if (this.controlKeyDown) { // ZOOM AROUND MOUSE POINTER - const zoomPosition = (ev.offsetX / this.stateController.zoomFactor); + const zoomPosition = BIMath.round(ev.offsetX / this.stateController.zoomFactor); const zoomIn = ev.deltaY < 0; - const newViewRangeLength = Math.max(1, Math.min(this.unitController.absoluteRange, - this.unitController.viewRangeLength * (zoomIn ? 0.8 : 1.25))); + const newViewRangeLength = BIMath.clamp(Number(this.unitController.viewRangeLength) * (zoomIn ? 0.8 : 1.25), + BigInt(1), this.unitController.absoluteRange); const center = this.unitController.viewRange.start + zoomPosition; - const start = Math.max(0, Math.min(this.unitController.absoluteRange - newViewRangeLength, - center - zoomPosition * newViewRangeLength / this.unitController.viewRangeLength)); + const start = BIMath.clamp(Number(center) - Number(zoomPosition) * Number(newViewRangeLength) / Number(this.unitController.viewRangeLength), + BigInt(0), this.unitController.absoluteRange - newViewRangeLength); const end = start + newViewRangeLength; this.unitController.viewRange = { start, @@ -68,14 +69,14 @@ export class TimeGraphAxis extends TimeGraphLayer { // PANNING const shiftStep = ev.deltaY; const oldViewRange = this.unitController.viewRange; - let start = oldViewRange.start + (shiftStep / this.stateController.zoomFactor); + let start = oldViewRange.start + BIMath.round(shiftStep / this.stateController.zoomFactor); if (start < 0) { - start = 0; + start = BigInt(0); } let end = start + this.unitController.viewRangeLength; if (end > this.unitController.absoluteRange) { start = this.unitController.absoluteRange - this.unitController.viewRangeLength; - end = start + this.unitController.viewRangeLength; + end = this.unitController.absoluteRange; } this.unitController.viewRange = { start, end } } diff --git a/timeline-chart/src/layer/time-graph-chart-arrows.ts b/timeline-chart/src/layer/time-graph-chart-arrows.ts index 95bc22b..2b23991 100644 --- a/timeline-chart/src/layer/time-graph-chart-arrows.ts +++ b/timeline-chart/src/layer/time-graph-chart-arrows.ts @@ -20,11 +20,11 @@ export class TimeGraphChartArrows extends TimeGraphChartLayer { protected getCoordinates(arrow: TimelineChart.TimeGraphArrow): TimeGraphArrowCoordinates { const relativeStartPosition = arrow.range.start - this.unitController.viewRange.start; const start: TimeGraphElementPosition = { - x: this.getPixels(relativeStartPosition), + x: this.getPixel(relativeStartPosition), y: (arrow.sourceId * this.rowController.rowHeight) + (this.rowController.rowHeight / 2) } const end: TimeGraphElementPosition = { - x: this.getPixels(relativeStartPosition + arrow.range.end - arrow.range.start), + x: this.getPixel(relativeStartPosition + arrow.range.end - arrow.range.start), y: (arrow.destinationId * this.rowController.rowHeight) + (this.rowController.rowHeight / 2) } return { start, end }; diff --git a/timeline-chart/src/layer/time-graph-chart-cursors.ts b/timeline-chart/src/layer/time-graph-chart-cursors.ts index 5120e36..6288c82 100644 --- a/timeline-chart/src/layer/time-graph-chart-cursors.ts +++ b/timeline-chart/src/layer/time-graph-chart-cursors.ts @@ -6,6 +6,7 @@ import { TimelineChart } from "../time-graph-model"; import { TimeGraphChartLayer } from "./time-graph-chart-layer"; import { TimeGraphRowController } from "../time-graph-row-controller"; import { TimeGraphChart } from "./time-graph-chart"; +import { BIMath } from "../bigint-utils"; export class TimeGraphChartCursors extends TimeGraphChartLayer { protected mouseSelecting: boolean = false; @@ -79,18 +80,18 @@ export class TimeGraphChartCursors extends TimeGraphChartLayer { this.mouseSelecting = true; this.stage.cursor = 'crosshair'; const mouseX = event.data.global.x; - const xpos = this.unitController.viewRange.start + (mouseX / this.stateController.zoomFactor); + const end = this.unitController.viewRange.start + BIMath.round(mouseX / this.stateController.zoomFactor); this.chartLayer.selectState(undefined); if (extendSelection) { - const start = this.unitController.selectionRange ? this.unitController.selectionRange.start : 0; + const start = this.unitController.selectionRange ? this.unitController.selectionRange.start : BigInt(0); this.unitController.selectionRange = { start, - end: xpos + end } } else { this.unitController.selectionRange = { - start: xpos, - end: xpos + start: end, + end: end } } }; @@ -108,11 +109,11 @@ export class TimeGraphChartCursors extends TimeGraphChartLayer { return; } const mouseX = event.data.global.x; - const xStartPos = this.unitController.selectionRange.start; - const xEndPos = this.unitController.viewRange.start + (mouseX / this.stateController.zoomFactor); + const start = this.unitController.selectionRange.start; + const end = this.unitController.viewRange.start + BIMath.round(mouseX / this.stateController.zoomFactor); this.unitController.selectionRange = { - start: xStartPos, - end: xEndPos + start, + end } } } @@ -235,21 +236,21 @@ export class TimeGraphChartCursors extends TimeGraphChartLayer { centerCursor() { if (this.unitController.selectionRange) { const cursorPosition = this.unitController.selectionRange.end; - const halfViewRangeLength = this.unitController.viewRangeLength / 2; - let startViewRange = cursorPosition - halfViewRangeLength; - let endViewRange = cursorPosition + halfViewRangeLength; + const halfViewRangeLength = this.unitController.viewRangeLength / BigInt(2); + let start = cursorPosition - halfViewRangeLength; + let end = cursorPosition + halfViewRangeLength; - if (startViewRange < 0) { - endViewRange -= startViewRange; - startViewRange = 0; - } else if (endViewRange > this.unitController.absoluteRange) { - startViewRange -= (endViewRange - this.unitController.absoluteRange); - endViewRange = this.unitController.absoluteRange; + if (start < 0) { + end -= start; + start = BigInt(0); + } else if (end > this.unitController.absoluteRange) { + start -= (end - this.unitController.absoluteRange); + end = this.unitController.absoluteRange; } this.unitController.viewRange = { - start: startViewRange, - end: endViewRange + start, + end } } } @@ -260,8 +261,8 @@ export class TimeGraphChartCursors extends TimeGraphChartLayer { update() { 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 firstCursorOptions = { color: this.color, height: this.stateController.canvasDisplayHeight, diff --git a/timeline-chart/src/layer/time-graph-chart-selection-range.ts b/timeline-chart/src/layer/time-graph-chart-selection-range.ts index 2f7dd66..b8a0998 100644 --- a/timeline-chart/src/layer/time-graph-chart-selection-range.ts +++ b/timeline-chart/src/layer/time-graph-chart-selection-range.ts @@ -17,8 +17,8 @@ export class TimeGraphChartSelectionRange extends TimeGraphLayer { protected updateScaleAndPosition() { if (this.unitController.selectionRange && this.selectionRange) { - const firstCursorPosition = this.getPixels(this.unitController.selectionRange.start - this.unitController.viewRange.start); - const width = this.getPixels(this.unitController.selectionRange.end - this.unitController.selectionRange.start) + const firstCursorPosition = this.getPixel(this.unitController.selectionRange.start - this.unitController.viewRange.start); + const width = this.getPixel(this.unitController.selectionRange.end - this.unitController.selectionRange.start) this.selectionRange.update({ position: { x: firstCursorPosition, @@ -48,8 +48,8 @@ export class TimeGraphChartSelectionRange extends TimeGraphLayer { update() { 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); if (secondCursorPosition !== firstCursorPosition) { if (!this.selectionRange) { this.selectionRange = new TimeGraphRectangle({ diff --git a/timeline-chart/src/layer/time-graph-chart.ts b/timeline-chart/src/layer/time-graph-chart.ts index 76076ce..c7e4eb5 100644 --- a/timeline-chart/src/layer/time-graph-chart.ts +++ b/timeline-chart/src/layer/time-graph-chart.ts @@ -7,6 +7,7 @@ import { TimeGraphStateComponent, TimeGraphStateStyle } from "../components/time import { TimelineChart } from "../time-graph-model"; import { TimeGraphRowController } from "../time-graph-row-controller"; import { TimeGraphChartLayer } from "./time-graph-chart-layer"; +import { BIMath } from "../bigint-utils"; export interface TimeGraphMouseInteractions { click?: (el: TimeGraphComponent, ev: PIXI.InteractionEvent) => void @@ -54,7 +55,8 @@ export class TimeGraphChart extends TimeGraphChartLayer { protected mouseDownButton: number; protected mouseStartX: number; protected mouseEndX: number; - protected mouseZoomingStart: number; + protected mousePanningStart: bigint; + protected mouseZoomingStart: bigint; protected zoomingSelection?: TimeGraphRectangle; private _stageMouseDownHandler: Function; @@ -73,26 +75,26 @@ export class TimeGraphChart extends TimeGraphChartLayer { protected providers: TimeGraphChartProviders, protected rowController: TimeGraphRowController) { super(id, rowController); - this.providedRange = { start: 0, end: 0 }; + this.providedRange = { start: BigInt(0), end: BigInt(0) }; this.providedResolution = 1; this.isNavigating = false; } adjustZoom(zoomPosition: number | undefined, hasZoomedIn: boolean) { if (zoomPosition === undefined) { - const start = this.getPixels(this.unitController.selectionRange ? this.unitController.selectionRange.start - this.unitController.viewRange.start : 0); - const end = this.getPixels(this.unitController.selectionRange ? this.unitController.selectionRange.end - this.unitController.viewRange.start : this.unitController.viewRangeLength); + const start = this.getPixel(this.unitController.selectionRange ? this.unitController.selectionRange.start - this.unitController.viewRange.start : BigInt(0)); + const end = this.getPixel(this.unitController.selectionRange ? this.unitController.selectionRange.end - this.unitController.viewRange.start : this.unitController.viewRangeLength); zoomPosition = (start + end) / 2; } - const zoomPixels = zoomPosition / this.stateController.zoomFactor; + const zoomTime = zoomPosition / this.stateController.zoomFactor; const zoomMagnitude = hasZoomedIn ? 0.8 : 1.25; - const newViewRangeLength = Math.max(1, Math.min(this.unitController.absoluteRange, - this.unitController.viewRangeLength * zoomMagnitude)); - const center = this.unitController.viewRange.start + zoomPixels; - const start = Math.max(0, Math.min(this.unitController.absoluteRange - newViewRangeLength, - center - zoomPixels * newViewRangeLength / this.unitController.viewRangeLength)); + const newViewRangeLength = BIMath.clamp(Number(this.unitController.viewRangeLength) * zoomMagnitude, + BigInt(2), this.unitController.absoluteRange); + const center = this.unitController.viewRange.start + BIMath.round(zoomTime); + const start = BIMath.clamp(Number(center) - zoomTime * Number(newViewRangeLength) / Number(this.unitController.viewRangeLength), + BigInt(0), this.unitController.absoluteRange - newViewRangeLength); const end = start + newViewRangeLength; - if (Math.trunc(start) !== Math.trunc(end)) { + if (start !== end) { this.unitController.viewRange = { start, end @@ -107,13 +109,26 @@ export class TimeGraphChart extends TimeGraphChartLayer { let triggerKeyEvent = false; const moveHorizontally = (magnitude: number) => { - const xOffset = -(magnitude / this.stateController.zoomFactor); - let start = Math.max(0, this.unitController.viewRange.start - xOffset); - let end = start + this.unitController.viewRangeLength; - if (end > this.unitController.absoluteRange) { - end = this.unitController.absoluteRange; - start = end - this.unitController.viewRangeLength; + if (magnitude === 0) { + return; } + // move by at least one nanosecond + const absOffset = BIMath.max(1, Math.abs(magnitude / this.stateController.zoomFactor)); + const timeOffset = magnitude > 0 ? absOffset : -absOffset; + const start = BIMath.clamp(this.unitController.viewRange.start + timeOffset, + BigInt(0), this.unitController.absoluteRange - this.unitController.viewRangeLength); + const end = start + this.unitController.viewRangeLength; + this.unitController.viewRange = { + start, + end + } + } + + const panHorizontally = (magnitude: number) => { + const timeOffset = BIMath.round(magnitude / this.stateController.zoomFactor); + const start = BIMath.clamp(this.mousePanningStart - timeOffset, + BigInt(0), this.unitController.absoluteRange - this.unitController.viewRangeLength); + const end = start + this.unitController.viewRangeLength; this.unitController.viewRange = { start, end @@ -195,6 +210,7 @@ export class TimeGraphChart extends TimeGraphChartLayer { this.mousePanning = true; this.mouseDownButton = event.data.button; this.mouseStartX = event.data.global.x; + this.mousePanningStart = this.unitController.viewRange.start; this.stage.cursor = 'grabbing'; }; this.stage.on('mousedown', this._stageMouseDownHandler); @@ -212,9 +228,8 @@ export class TimeGraphChart extends TimeGraphChartLayer { } return; } - const horizontalDelta = this.mouseStartX - event.data.global.x; - moveHorizontally(horizontalDelta); - this.mouseStartX = event.data.global.x; + const horizontalDelta = event.data.global.x - this.mouseStartX; + panHorizontally(horizontalDelta); } if (this.mouseZooming) { this.mouseEndX = event.data.global.x; @@ -264,7 +279,7 @@ export class TimeGraphChart extends TimeGraphChartLayer { this.mouseDownButton = e.button; this.mouseStartX = e.offsetX; this.mouseEndX = e.offsetX; - this.mouseZoomingStart = this.unitController.viewRange.start + (this.mouseStartX / this.stateController.zoomFactor); + this.mouseZoomingStart = this.unitController.viewRange.start + BIMath.round(this.mouseStartX / this.stateController.zoomFactor); this.stage.cursor = 'col-resize'; // this is the only way to detect mouseup outside of right button document.addEventListener('mouseup', mouseUpListener); @@ -276,15 +291,10 @@ export class TimeGraphChart extends TimeGraphChartLayer { if (e.button === this.mouseDownButton && this.mouseZooming) { this.mouseZooming = false; const start = this.mouseZoomingStart; - const end = this.unitController.viewRange.start + (this.mouseEndX / this.stateController.zoomFactor); - if (start !== end && this.unitController.viewRangeLength > 1) { - let newViewStart = Math.max(Math.min(start, end), this.unitController.viewRange.start); - let newViewEnd = Math.min(Math.max(start, end), this.unitController.viewRange.end); - if (newViewEnd - newViewStart < 1) { - const center = (newViewStart + newViewEnd) / 2; - newViewStart = center - 0.5; - newViewEnd = center + 0.5; - } + const end = this.unitController.viewRange.start + BIMath.round(this.mouseEndX / this.stateController.zoomFactor); + if (BIMath.abs(end - start) > 1 && this.unitController.viewRangeLength > 1) { + let newViewStart = BIMath.clamp(start, this.unitController.viewRange.start, end); + let newViewEnd = BIMath.clamp(end, start, this.unitController.viewRange.end); this.unitController.viewRange = { start: newViewStart, end: newViewEnd @@ -309,7 +319,7 @@ export class TimeGraphChart extends TimeGraphChartLayer { this._viewRangeChangedHandler = () => { this.updateScaleAndPosition(); - if (!this.fetching && this.unitController.viewRangeLength !== 0) { + if (!this.fetching && this.unitController.viewRangeLength !== BigInt(0)) { this.maybeFetchNewData(); } if (this.mouseZooming) { @@ -339,7 +349,7 @@ export class TimeGraphChart extends TimeGraphChartLayer { delete this.zoomingSelection; } if (this.mouseZooming) { - const mouseStartX = (this.mouseZoomingStart - this.unitController.viewRange.start) * this.stateController.zoomFactor; + const mouseStartX = Number(this.mouseZoomingStart - this.unitController.viewRange.start) * this.stateController.zoomFactor; this.zoomingSelection = new TimeGraphRectangle({ color: 0xbbbbbb, opacity: 0.2, @@ -387,7 +397,7 @@ export class TimeGraphChart extends TimeGraphChartLayer { } protected async maybeFetchNewData(update?: boolean) { - const resolution = this.unitController.viewRangeLength / this.stateController.canvasDisplayWidth; + const resolution = Number(this.unitController.viewRangeLength) / this.stateController.canvasDisplayWidth; const viewRange = this.unitController.viewRange; if (viewRange && ( viewRange.start < this.providedRange.start || @@ -442,12 +452,12 @@ export class TimeGraphChart extends TimeGraphChartLayer { const opts: TimeGraphStyledRect = { height: el.height, position: { - x: this.getPixels(start - this.unitController.viewRange.start), + x: this.getPixel(start - this.unitController.viewRange.start), y: el.position.y }, // min width of a state should never be less than 1 (for visibility) - width: Math.max(1, this.getPixels(end) - this.getPixels(start)), - displayWidth: this.getPixels(Math.min(this.unitController.viewRange.end, end)) - this.getPixels(Math.max(this.unitController.viewRange.start, start)) + width: Math.max(1, this.getPixel(end) - this.getPixel(start)), + displayWidth: this.getPixel(BIMath.min(this.unitController.viewRange.end, end)) - this.getPixel(BIMath.max(this.unitController.viewRange.start, start)) } el.update(opts); } @@ -459,7 +469,7 @@ export class TimeGraphChart extends TimeGraphChartLayer { const start = annotation.range.start; const opts: TimeGraphAnnotationComponentOptions = { position: { - x: this.getPixels(start - this.unitController.viewRange.start), + x: this.getPixel(start - this.unitController.viewRange.start), y: el.displayObject.y } } @@ -517,7 +527,7 @@ export class TimeGraphChart extends TimeGraphChartLayer { } protected createNewAnnotation(annotation: TimelineChart.TimeGraphAnnotation, rowComponent: TimeGraphRow) { - const start = this.getPixels(annotation.range.start - this.unitController.viewRange.start); + const start = this.getPixel(annotation.range.start - this.unitController.viewRange.start); let el: TimeGraphAnnotationComponent | undefined; const elementStyle = this.providers.rowAnnotationStyleProvider ? this.providers.rowAnnotationStyleProvider(annotation) : undefined; el = new TimeGraphAnnotationComponent(annotation.id, annotation, { position: { x: start, y: rowComponent.position.y + (rowComponent.height * 0.5) } }, elementStyle, rowComponent); @@ -526,18 +536,14 @@ export class TimeGraphChart extends TimeGraphChartLayer { } protected createNewState(stateModel: TimelineChart.TimeGraphState, rowComponent: TimeGraphRow): TimeGraphStateComponent | undefined { - const start = this.getPixels(stateModel.range.start - this.unitController.viewRange.start); - const end = this.getPixels(stateModel.range.end - this.unitController.viewRange.start); + const xStart = this.getPixel(stateModel.range.start - this.unitController.viewRange.start); + const xEnd = this.getPixel(stateModel.range.end - this.unitController.viewRange.start); let el: TimeGraphStateComponent | undefined; - const range: TimelineChart.TimeGraphRange = { - start, - end - }; - const displayStart = this.getPixels(Math.max(stateModel.range.start, this.unitController.viewRange.start)); - const displayEnd = this.getPixels(Math.min(stateModel.range.end, this.unitController.viewRange.end)); + const displayStart = this.getPixel(BIMath.max(stateModel.range.start, this.unitController.viewRange.start)); + const displayEnd = this.getPixel(BIMath.min(stateModel.range.end, this.unitController.viewRange.end)); const displayWidth = displayEnd - displayStart; const elementStyle = this.providers.stateStyleProvider ? this.providers.stateStyleProvider(stateModel) : undefined; - el = new TimeGraphStateComponent(stateModel.id, stateModel, range, rowComponent, elementStyle, displayWidth); + el = new TimeGraphStateComponent(stateModel.id, stateModel, xStart, xEnd, rowComponent, elementStyle, displayWidth); this.rowStateComponents.set(stateModel, el); return el; } diff --git a/timeline-chart/src/layer/time-graph-layer.ts b/timeline-chart/src/layer/time-graph-layer.ts index 3af9dcf..50c1e8c 100644 --- a/timeline-chart/src/layer/time-graph-layer.ts +++ b/timeline-chart/src/layer/time-graph-layer.ts @@ -63,8 +63,11 @@ export abstract class TimeGraphLayer { idx && this.children.splice(idx, 1); } - protected getPixels(ticks: number) { - return ticks * this.stateController.zoomFactor; + protected getPixel(time: bigint) { + const div = 0x100000000; + const hi = Number(time / BigInt(div)); + const lo = Number(time % BigInt(div)); + return Math.floor(hi * this.stateController.zoomFactor * div + lo * this.stateController.zoomFactor); } protected afterAddToContainer() { } diff --git a/timeline-chart/src/layer/time-graph-navigator.ts b/timeline-chart/src/layer/time-graph-navigator.ts index bbf28eb..da84518 100644 --- a/timeline-chart/src/layer/time-graph-navigator.ts +++ b/timeline-chart/src/layer/time-graph-navigator.ts @@ -4,6 +4,7 @@ import { TimeGraphStateController } from "../time-graph-state-controller"; import { TimeGraphRectangle } from "../components/time-graph-rectangle"; import { TimeGraphLayer } from "./time-graph-layer"; import { TimelineChart } from "../time-graph-model"; +import { BIMath } from "../bigint-utils"; export class TimeGraphNavigator extends TimeGraphLayer { @@ -30,10 +31,10 @@ export class TimeGraphNavigator extends TimeGraphLayer { height: this.stateController.canvasDisplayHeight, opacity: 0.5, position: { - x: this.unitController.selectionRange.start * this.stateController.absoluteResolution, + x: Number(this.unitController.selectionRange.start) * this.stateController.absoluteResolution, y: 0 }, - width: (this.unitController.selectionRange.end - this.unitController.selectionRange.start) * this.stateController.absoluteResolution + width: Number(this.unitController.selectionRange.end - this.unitController.selectionRange.start) * this.stateController.absoluteResolution }; if (!this.selectionRange) { this.selectionRange = new TimeGraphRectangle(selectionOpts); @@ -61,7 +62,7 @@ export class TimeGraphNavigatorHandle extends TimeGraphComponent { protected mouseIsDown: boolean; protected mouseStartX: number; - protected oldViewStart: number; + protected oldViewStart: bigint; constructor(protected unitController: TimeGraphUnitController, protected stateController: TimeGraphStateController) { super('navigator_handle'); @@ -73,9 +74,9 @@ export class TimeGraphNavigatorHandle extends TimeGraphComponent { this.addEvent('mousemove', event => { if (this.mouseIsDown) { const delta = event.data.global.x - this.mouseStartX; - var start = Math.max(this.oldViewStart + (delta / this.stateController.absoluteResolution), 0); - start = Math.min(start, this.unitController.absoluteRange - this.unitController.viewRangeLength); - const end = Math.min(start + this.unitController.viewRangeLength, this.unitController.absoluteRange) + const start = BIMath.clamp(Number(this.oldViewStart) + (delta / this.stateController.absoluteResolution), + BigInt(0), this.unitController.absoluteRange - this.unitController.viewRangeLength); + const end = start + this.unitController.viewRangeLength; this.unitController.viewRange = { start, end @@ -91,11 +92,11 @@ export class TimeGraphNavigatorHandle extends TimeGraphComponent { render(): void { const MIN_NAVIGATOR_WIDTH = 20; - const xPos = this.unitController.viewRange.start * this.stateController.absoluteResolution; - const effectiveAbsoluteRange = this.unitController.absoluteRange * this.stateController.absoluteResolution; + const xPos = Number(this.unitController.viewRange.start) * this.stateController.absoluteResolution; + const effectiveAbsoluteRange = Number(this.unitController.absoluteRange) * this.stateController.absoluteResolution; // Avoid the navigator rendered outside of the range at high zoom levels when its width is capped to MIN_NAVIGATOR_WIDTH const position = { x: Math.min(effectiveAbsoluteRange - MIN_NAVIGATOR_WIDTH, xPos), y: 0 }; - const width = Math.max(MIN_NAVIGATOR_WIDTH, this.unitController.viewRangeLength * this.stateController.absoluteResolution); + const width = Math.max(MIN_NAVIGATOR_WIDTH, Number(this.unitController.viewRangeLength) * this.stateController.absoluteResolution); this.rect({ height: 20, position, diff --git a/timeline-chart/src/layer/time-graph-range-events-layer.ts b/timeline-chart/src/layer/time-graph-range-events-layer.ts index caad503..0c2734d 100644 --- a/timeline-chart/src/layer/time-graph-range-events-layer.ts +++ b/timeline-chart/src/layer/time-graph-range-events-layer.ts @@ -21,8 +21,8 @@ export class TimeGraphRangeEventsLayer extends TimeGraphLayer { } protected addRangeEvent(rangeEvent: TimelineChart.TimeGraphAnnotation) { - const start = this.getPixels(rangeEvent.range.start - this.unitController.viewRange.start); - const end = this.getPixels(rangeEvent.range.end - this.unitController.viewRange.start); + const start = this.getPixel(rangeEvent.range.start - this.unitController.viewRange.start); + const end = this.getPixel(rangeEvent.range.end - this.unitController.viewRange.start); const width = end - start; const elementStyle = this.providers.rowAnnotationStyleProvider ? this.providers.rowAnnotationStyleProvider(rangeEvent) : undefined; const rangeEventComponent = new TimeGraphRectangle({ @@ -62,8 +62,8 @@ export class TimeGraphRangeEventsLayer extends TimeGraphLayer { protected updateRangeEvent(rangeEvent: TimelineChart.TimeGraphAnnotation) { const rangeEventComponent = this.rangeEvents.get(rangeEvent); - const start = this.getPixels(rangeEvent.range.start - this.unitController.viewRange.start); - const end = this.getPixels(rangeEvent.range.end - this.unitController.viewRange.start); + const start = this.getPixel(rangeEvent.range.start - this.unitController.viewRange.start); + const end = this.getPixel(rangeEvent.range.end - this.unitController.viewRange.start); const width = end - start; if (rangeEventComponent) { rangeEventComponent.update({ diff --git a/timeline-chart/src/time-graph-model.ts b/timeline-chart/src/time-graph-model.ts index 5b4dd1c..0dd2cba 100644 --- a/timeline-chart/src/time-graph-model.ts +++ b/timeline-chart/src/time-graph-model.ts @@ -1,12 +1,12 @@ export namespace TimelineChart { export interface TimeGraphRange { - start: number - end: number + start: bigint + end: bigint } export interface TimeGraphModel { id: string - totalLength: number + totalLength: bigint rows: TimeGraphRowModel[] rangeEvents: TimeGraphAnnotation[] arrows: TimeGraphArrow[] @@ -21,8 +21,8 @@ export namespace TimelineChart { annotations: TimeGraphAnnotation[] selected?: boolean readonly data?: { [key: string]: any } - prevPossibleState: number - nextPossibleState: number + prevPossibleState: bigint + nextPossibleState: bigint } export interface TimeGraphState { diff --git a/timeline-chart/src/time-graph-state-controller.ts b/timeline-chart/src/time-graph-state-controller.ts index e2c6860..c7e9762 100644 --- a/timeline-chart/src/time-graph-state-controller.ts +++ b/timeline-chart/src/time-graph-state-controller.ts @@ -81,12 +81,12 @@ export class TimeGraphStateController { } get zoomFactor(): number { - this._zoomFactor = this.canvasDisplayWidth / this.unitController.viewRangeLength; + this._zoomFactor = this.canvasDisplayWidth / Number(this.unitController.viewRangeLength); return this._zoomFactor; } get absoluteResolution(): number { - return this.canvasDisplayWidth / this.unitController.absoluteRange; + return this.canvasDisplayWidth / Number(this.unitController.absoluteRange); } get positionOffset(): { diff --git a/timeline-chart/src/time-graph-unit-controller.ts b/timeline-chart/src/time-graph-unit-controller.ts index 7fa467d..fdbbf9e 100644 --- a/timeline-chart/src/time-graph-unit-controller.ts +++ b/timeline-chart/src/time-graph-unit-controller.ts @@ -11,12 +11,12 @@ export class TimeGraphUnitController { * Create a string from the given number, which is shown in TimeAxis. * Or return undefined to not show any text for that number. */ - numberTranslator?: (theNumber: number) => string | undefined; + numberTranslator?: (theNumber: bigint) => string | undefined; scaleSteps?: number[] - constructor(public absoluteRange: number, viewRange?: TimelineChart.TimeGraphRange) { + constructor(public absoluteRange: bigint, viewRange?: TimelineChart.TimeGraphRange) { this.viewRangeChangedHandlers = []; - this._viewRange = viewRange || { start: 0, end: absoluteRange }; + this._viewRange = viewRange || { start: BigInt(0), end: absoluteRange }; this.selectionRangeChangedHandlers = []; } @@ -59,7 +59,7 @@ export class TimeGraphUnitController { this._viewRange = { start: newRange.start, end: newRange.end }; } if (newRange.start < 0) { - this._viewRange.start = 0; + this._viewRange.start = BigInt(0); } if (this._viewRange.end > this.absoluteRange) { this._viewRange.end = this.absoluteRange; @@ -75,7 +75,7 @@ export class TimeGraphUnitController { this.handleSelectionRangeChange(); } - get viewRangeLength(): number { + get viewRangeLength(): bigint { return this._viewRange.end - this._viewRange.start; } } \ No newline at end of file diff --git a/timeline-chart/tsconfig.json b/timeline-chart/tsconfig.json index b0cac07..5edb764 100644 --- a/timeline-chart/tsconfig.json +++ b/timeline-chart/tsconfig.json @@ -15,7 +15,8 @@ "jsx": "react", "lib": [ "es6", - "dom" + "dom", + "esnext.bigint" ], "declaration": true, "sourceMap": true,