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,