From c39daabdab8c118075954407591d644a2a5f5fc9 Mon Sep 17 00:00:00 2001 From: panoreak Date: Tue, 4 Feb 2020 15:42:00 -0500 Subject: [PATCH 1/7] [#157] Replaced Date objects with NanoDate objects --- app/src/utils/date-utils.ts | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/app/src/utils/date-utils.ts b/app/src/utils/date-utils.ts index f1fcae08..98e11b0c 100644 --- a/app/src/utils/date-utils.ts +++ b/app/src/utils/date-utils.ts @@ -115,9 +115,8 @@ export const getLocalTimeString = (nanosecondTimestamp: string) : string => { * @return Date */ export const getDateObjectForGraphScale = (nanosecondTimestamp: bigInt.BigInteger) => { - const localTimezoneDate = new Date(); + const localTimezoneDate = new NanoDate(); const localTimezoneOffsetInNano = localTimezoneDate.getTimezoneOffset(); - return new Date(Number(nanosecondTimestamp - .plus((localTimezoneOffsetInNano - EDT_TIMEZONE_OFFSET_IN_MINUTES) * 60 * 10 ** 9) - .divide(1000000))); + return new NanoDate((nanosecondTimestamp + .plus((localTimezoneOffsetInNano - EDT_TIMEZONE_OFFSET_IN_MINUTES) * 60 * 10 ** 9)).toString()); }; From ec17f3263417f2151f8be543e2578520ef36e535 Mon Sep 17 00:00:00 2001 From: panoreak Date: Fri, 7 Feb 2020 22:06:38 -0500 Subject: [PATCH 2/7] [#157] Initial commit for zoom/panning event handling --- app/src/components/OrderBookSnapshot.tsx | 52 +++++++++++--- app/src/components/TopOfBookGraph.tsx | 75 ++++++++++++++++++-- app/src/components/TopOfBookGraphWrapper.tsx | 8 ++- 3 files changed, 120 insertions(+), 15 deletions(-) diff --git a/app/src/components/OrderBookSnapshot.tsx b/app/src/components/OrderBookSnapshot.tsx index 5db6c058..b9adf966 100644 --- a/app/src/components/OrderBookSnapshot.tsx +++ b/app/src/components/OrderBookSnapshot.tsx @@ -49,6 +49,8 @@ interface State { loadingOrderbook: boolean, loadingGraph: boolean, graphUnavailable: boolean, + graphStartTime: bigInt.BigInteger, + graphEndTime: bigInt.BigInteger, } class OrderBookSnapshot extends Component { @@ -56,11 +58,9 @@ class OrderBookSnapshot extends Component { * @desc Handles window resizing and requests a new number of data points appropriate for the new window width */ handleResize = debounce(() => { - const { selectedDateNano } = this.state; + const { selectedDateNano, graphStartTime, graphEndTime } = this.state; if (selectedDateNano.neq(0)) { - const startTime = selectedDateNano.plus(NANOSECONDS_IN_NINE_AND_A_HALF_HOURS); - const endTime = selectedDateNano.plus(NANOSECONDS_IN_SIXTEEN_HOURS); - this.updateGraphData(startTime, endTime); + this.updateGraphData(graphStartTime, graphEndTime); } }, 100); @@ -82,6 +82,8 @@ class OrderBookSnapshot extends Component { loadingOrderbook: false, loadingGraph: false, graphUnavailable: false, + graphStartTime: bigInt(0), + graphEndTime: bigInt(0), }; } @@ -125,9 +127,16 @@ class OrderBookSnapshot extends Component { () => { const { selectedDateNano } = this.state; if (selectedDateNano.neq(0)) { - const startTime = selectedDateNano.plus(NANOSECONDS_IN_NINE_AND_A_HALF_HOURS); - const endTime = selectedDateNano.plus(NANOSECONDS_IN_SIXTEEN_HOURS); - this.updateGraphData(startTime, endTime); + const graphStartTime = selectedDateNano.plus(NANOSECONDS_IN_NINE_AND_A_HALF_HOURS); + const graphEndTime = selectedDateNano.plus(NANOSECONDS_IN_SIXTEEN_HOURS); + this.setState( + { + graphStartTime, + graphEndTime, + }, () => { + this.updateGraphData(graphStartTime, graphEndTime); + }, + ); } }, ); @@ -161,8 +170,8 @@ class OrderBookSnapshot extends Component { const selectedDateNano = convertNanosecondsToUTC(dateStringToEpoch(`${selectedDateString} 00:00:00`)); const selectedDateTimeNano = selectedDateNano.plus(selectedTimeNano); - const startTime = selectedDateNano.plus(NANOSECONDS_IN_NINE_AND_A_HALF_HOURS); - const endTime = selectedDateNano.plus(NANOSECONDS_IN_SIXTEEN_HOURS); + const graphStartTime = selectedDateNano.plus(NANOSECONDS_IN_NINE_AND_A_HALF_HOURS); + const graphEndTime = selectedDateNano.plus(NANOSECONDS_IN_SIXTEEN_HOURS); this.setState( { @@ -170,10 +179,12 @@ class OrderBookSnapshot extends Component { selectedTimeString, selectedDateNano, selectedDateTimeNano, + graphStartTime, + graphEndTime, }, () => { this.handleChangeDateTime(); - this.updateGraphData(startTime, endTime); + this.updateGraphData(graphStartTime, graphEndTime); }, ); }; @@ -270,6 +281,7 @@ class OrderBookSnapshot extends Component { */ updateGraphData = (startTime: bigInt.BigInteger, endTime: bigInt.BigInteger) => { const { selectedInstrument } = this.state; + console.debug(startTime, endTime); OrderBookService.getTopOfBookOverTime(selectedInstrument, startTime.toString(), endTime.toString(), this.getNumDataPoints()) @@ -297,12 +309,29 @@ class OrderBookSnapshot extends Component { }); }; + /** + * @desc handles updating the graph when zooming or panning the graph + * @param graphStartTime the new start time on the graph + * @param graphEndTime the new end time on the graph + */ + handlePanAndZoom = (graphStartTime: bigInt.BigInteger, graphEndTime: bigInt.BigInteger) => { + this.setState( + { + graphStartTime, + graphEndTime, + }, () => { + this.updateGraphData(graphStartTime, graphEndTime); + }, + ); + }; + render() { const { classes } = this.props; const { listItems, maxQuantity, selectedDateTimeNano, + selectedDateNano, datePickerValue, selectedTimeString, lastSodOffset, @@ -451,7 +480,10 @@ class OrderBookSnapshot extends Component { )} diff --git a/app/src/components/TopOfBookGraph.tsx b/app/src/components/TopOfBookGraph.tsx index 64a100ef..f2941777 100644 --- a/app/src/components/TopOfBookGraph.tsx +++ b/app/src/components/TopOfBookGraph.tsx @@ -18,25 +18,92 @@ import { } from 'react-stockcharts/lib/coordinates'; import bigInt from 'big-integer'; +import { debounce } from 'lodash'; import { getDateObjectForGraphScale, getLocalTimeString } from '../utils/date-utils'; import { Colors } from '../styles/App'; import { TopOfBookItem } from '../models/OrderBook'; +import { + NANOSECONDS_IN_ONE_MILLISECOND, +} from '../constants/Constants'; +interface State { + graphStartTime: bigInt.BigInteger, + graphEndTime: bigInt.BigInteger, +} interface Props { height: number, width: number, onTimeSelect: (any) => void, selectedDateTimeNano: bigInt.BigInteger, + startOfDay: bigInt.BigInteger, + endOfDay: bigInt.BigInteger, topOfBookItems: Array, + handlePanAndZoom: (graphStartTime: bigInt.BigInteger, graphEndTime: bigInt.BigInteger) => void, } -class TopOfBookGraph extends Component { +class TopOfBookGraph extends Component { + private chartCanvasRef: any; + + handleEvents = debounce((type, moreProps) => { + if (type === 'panend' || type === 'zoom') { + const { handlePanAndZoom, startOfDay, endOfDay } = this.props; + + const graphDomain = moreProps.xScale.domain(); + let graphStartTime = bigInt(graphDomain[0].getTime() * NANOSECONDS_IN_ONE_MILLISECOND); + let graphEndTime = bigInt(graphDomain[1].getTime() * NANOSECONDS_IN_ONE_MILLISECOND); + + graphStartTime = graphStartTime.lesser(startOfDay) ? startOfDay : graphStartTime; + graphEndTime = graphEndTime.greater(endOfDay) ? endOfDay : graphEndTime; + + this.setState( + { + graphStartTime, + graphEndTime, + }, () => { + handlePanAndZoom(graphStartTime, graphEndTime); + }, + ); + } + }, 200); + + constructor(props) { + super(props); + + const { startOfDay, endOfDay } = props; + + this.state = { + graphStartTime: startOfDay, + graphEndTime: endOfDay, + + }; + } + + componentDidMount() { + this.chartCanvasRef.subscribe('chartCanvasEvents', { listener: this.handleEvents }); + } + + componentWillUnmount() { + this.chartCanvasRef.unsubscribe('chartCanvasEvents'); + } + render() { + const { graphStartTime, graphEndTime } = this.state; + console.log(graphEndTime, graphStartTime); const { - width, height, onTimeSelect, selectedDateTimeNano, topOfBookItems, + width, height, onTimeSelect, selectedDateTimeNano, topOfBookItems, startOfDay, endOfDay, } = this.props; + let clampValue = ''; + if (startOfDay.equals(graphStartTime)) { + clampValue = 'left'; + if (endOfDay.equals(graphEndTime)) { + clampValue = 'both'; + } + } else if (endOfDay.equals(graphEndTime)) { + clampValue = 'right'; + } + topOfBookItems.forEach(element => { // @ts-ignore // eslint-disable-next-line no-param-reassign @@ -51,6 +118,7 @@ class TopOfBookGraph extends Component { return ( { this.chartCanvasRef = node; }} width={width} height={height} ratio={width / height} @@ -61,8 +129,7 @@ class TopOfBookGraph extends Component { displayXAccessor={d => d.timestamp} xAccessor={d => d.date} xScale={scaleTime()} - panEvent={false} - zoomEvent={false} + clamp={clampValue === '' ? false : clampValue} xExtents={[data[0].date, data[data.length - 1].date]} > { className: string, onTimeSelect: (any) => void, selectedDateTimeNano: bigInt.BigInteger, + startOfDay: bigInt.BigInteger, + endOfDay: bigInt.BigInteger, topOfBookItems: Array, + handlePanAndZoom: (graphStartTime: bigInt.BigInteger, graphEndTime: bigInt.BigInteger) => void, } interface State { @@ -59,7 +62,7 @@ class TopOfBookGraphWrapper extends Component { render() { const { - classes, onTimeSelect, selectedDateTimeNano, topOfBookItems, + classes, onTimeSelect, selectedDateTimeNano, topOfBookItems, handlePanAndZoom, startOfDay, endOfDay, } = this.props; const { graphWidth, graphHeight } = this.state; return ( @@ -74,7 +77,10 @@ class TopOfBookGraphWrapper extends Component { width={graphWidth} onTimeSelect={onTimeSelect} selectedDateTimeNano={selectedDateTimeNano} + startOfDay={startOfDay} + endOfDay={endOfDay} topOfBookItems={topOfBookItems} + handlePanAndZoom={handlePanAndZoom} /> )} From 6408d575bf82d44f0c413d67ad3280848cc02a44 Mon Sep 17 00:00:00 2001 From: panoreak Date: Fri, 7 Feb 2020 22:35:02 -0500 Subject: [PATCH 3/7] [#157] Added function in date utils that converts a date object from the graph's x-axis to a correct timestamp for the backend --- app/src/components/TopOfBookGraph.tsx | 9 +++------ app/src/utils/date-utils.ts | 12 ++++++++++++ 2 files changed, 15 insertions(+), 6 deletions(-) diff --git a/app/src/components/TopOfBookGraph.tsx b/app/src/components/TopOfBookGraph.tsx index f2941777..ee816187 100644 --- a/app/src/components/TopOfBookGraph.tsx +++ b/app/src/components/TopOfBookGraph.tsx @@ -19,12 +19,9 @@ import { import bigInt from 'big-integer'; import { debounce } from 'lodash'; -import { getDateObjectForGraphScale, getLocalTimeString } from '../utils/date-utils'; +import { getDateObjectForGraphScale, getLocalTimeString, getTimestampForBackend } from '../utils/date-utils'; import { Colors } from '../styles/App'; import { TopOfBookItem } from '../models/OrderBook'; -import { - NANOSECONDS_IN_ONE_MILLISECOND, -} from '../constants/Constants'; interface State { graphStartTime: bigInt.BigInteger, @@ -50,8 +47,8 @@ class TopOfBookGraph extends Component { const { handlePanAndZoom, startOfDay, endOfDay } = this.props; const graphDomain = moreProps.xScale.domain(); - let graphStartTime = bigInt(graphDomain[0].getTime() * NANOSECONDS_IN_ONE_MILLISECOND); - let graphEndTime = bigInt(graphDomain[1].getTime() * NANOSECONDS_IN_ONE_MILLISECOND); + let graphStartTime = getTimestampForBackend(graphDomain[0]); + let graphEndTime = getTimestampForBackend(graphDomain[1]); graphStartTime = graphStartTime.lesser(startOfDay) ? startOfDay : graphStartTime; graphEndTime = graphEndTime.greater(endOfDay) ? endOfDay : graphEndTime; diff --git a/app/src/utils/date-utils.ts b/app/src/utils/date-utils.ts index 98e11b0c..e6734167 100644 --- a/app/src/utils/date-utils.ts +++ b/app/src/utils/date-utils.ts @@ -120,3 +120,15 @@ export const getDateObjectForGraphScale = (nanosecondTimestamp: bigInt.BigIntege return new NanoDate((nanosecondTimestamp .plus((localTimezoneOffsetInNano - EDT_TIMEZONE_OFFSET_IN_MINUTES) * 60 * 10 ** 9)).toString()); }; + +/** + * @desc Given a Date object from the graph's x-axis, returns a nanosecond timestamp with correct back-end timezone + * @param graphDate {bigInt} + * @return bigInt + */ +export const getTimestampForBackend = (graphDate: Date) => { + const localTimezoneDate = new NanoDate(); + const localTimezoneOffsetInNano = localTimezoneDate.getTimezoneOffset(); + return bigInt((graphDate.getTime() * NANOSECONDS_IN_ONE_MILLISECOND)) + .minus((localTimezoneOffsetInNano - EDT_TIMEZONE_OFFSET_IN_MINUTES) * 60 * 10 ** 9); +}; From dfd953d152bd2166269c277e7b29c273c622ffb9 Mon Sep 17 00:00:00 2001 From: panoreak Date: Fri, 7 Feb 2020 23:51:31 -0500 Subject: [PATCH 4/7] [#157] Removed clamp prop from graph, fixed render issue --- app/src/components/OrderBookSnapshot.tsx | 16 +++------ app/src/components/TopOfBookGraph.tsx | 44 ++---------------------- 2 files changed, 8 insertions(+), 52 deletions(-) diff --git a/app/src/components/OrderBookSnapshot.tsx b/app/src/components/OrderBookSnapshot.tsx index b9adf966..d5c6f57a 100644 --- a/app/src/components/OrderBookSnapshot.tsx +++ b/app/src/components/OrderBookSnapshot.tsx @@ -279,11 +279,10 @@ class OrderBookSnapshot extends Component { /** * @desc Updates the graph with tob values for new start time and end time bounds */ - updateGraphData = (startTime: bigInt.BigInteger, endTime: bigInt.BigInteger) => { + updateGraphData = (graphStartTime: bigInt.BigInteger, graphEndTime: bigInt.BigInteger) => { const { selectedInstrument } = this.state; - console.debug(startTime, endTime); - OrderBookService.getTopOfBookOverTime(selectedInstrument, startTime.toString(), endTime.toString(), + OrderBookService.getTopOfBookOverTime(selectedInstrument, graphStartTime.toString(), graphEndTime.toString(), this.getNumDataPoints()) .then(response => { // eslint-disable-next-line camelcase @@ -291,6 +290,8 @@ class OrderBookSnapshot extends Component { this.setState( { + graphStartTime, + graphEndTime, topOfBookItems: result, loadingGraph: false, }, @@ -315,14 +316,7 @@ class OrderBookSnapshot extends Component { * @param graphEndTime the new end time on the graph */ handlePanAndZoom = (graphStartTime: bigInt.BigInteger, graphEndTime: bigInt.BigInteger) => { - this.setState( - { - graphStartTime, - graphEndTime, - }, () => { - this.updateGraphData(graphStartTime, graphEndTime); - }, - ); + this.updateGraphData(graphStartTime, graphEndTime); }; render() { diff --git a/app/src/components/TopOfBookGraph.tsx b/app/src/components/TopOfBookGraph.tsx index ee816187..57b0122f 100644 --- a/app/src/components/TopOfBookGraph.tsx +++ b/app/src/components/TopOfBookGraph.tsx @@ -23,11 +23,6 @@ import { getDateObjectForGraphScale, getLocalTimeString, getTimestampForBackend import { Colors } from '../styles/App'; import { TopOfBookItem } from '../models/OrderBook'; -interface State { - graphStartTime: bigInt.BigInteger, - graphEndTime: bigInt.BigInteger, -} - interface Props { height: number, width: number, @@ -39,7 +34,7 @@ interface Props { handlePanAndZoom: (graphStartTime: bigInt.BigInteger, graphEndTime: bigInt.BigInteger) => void, } -class TopOfBookGraph extends Component { +class TopOfBookGraph extends Component { private chartCanvasRef: any; handleEvents = debounce((type, moreProps) => { @@ -53,29 +48,10 @@ class TopOfBookGraph extends Component { graphStartTime = graphStartTime.lesser(startOfDay) ? startOfDay : graphStartTime; graphEndTime = graphEndTime.greater(endOfDay) ? endOfDay : graphEndTime; - this.setState( - { - graphStartTime, - graphEndTime, - }, () => { - handlePanAndZoom(graphStartTime, graphEndTime); - }, - ); + handlePanAndZoom(graphStartTime, graphEndTime); } }, 200); - constructor(props) { - super(props); - - const { startOfDay, endOfDay } = props; - - this.state = { - graphStartTime: startOfDay, - graphEndTime: endOfDay, - - }; - } - componentDidMount() { this.chartCanvasRef.subscribe('chartCanvasEvents', { listener: this.handleEvents }); } @@ -85,22 +61,9 @@ class TopOfBookGraph extends Component { } render() { - const { graphStartTime, graphEndTime } = this.state; - console.log(graphEndTime, graphStartTime); const { - width, height, onTimeSelect, selectedDateTimeNano, topOfBookItems, startOfDay, endOfDay, + width, height, onTimeSelect, selectedDateTimeNano, topOfBookItems, } = this.props; - - let clampValue = ''; - if (startOfDay.equals(graphStartTime)) { - clampValue = 'left'; - if (endOfDay.equals(graphEndTime)) { - clampValue = 'both'; - } - } else if (endOfDay.equals(graphEndTime)) { - clampValue = 'right'; - } - topOfBookItems.forEach(element => { // @ts-ignore // eslint-disable-next-line no-param-reassign @@ -126,7 +89,6 @@ class TopOfBookGraph extends Component { displayXAccessor={d => d.timestamp} xAccessor={d => d.date} xScale={scaleTime()} - clamp={clampValue === '' ? false : clampValue} xExtents={[data[0].date, data[data.length - 1].date]} > Date: Sat, 8 Feb 2020 18:13:46 -0500 Subject: [PATCH 5/7] [#157] Reduced debounce time for zooming/panning --- app/src/components/TopOfBookGraph.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/components/TopOfBookGraph.tsx b/app/src/components/TopOfBookGraph.tsx index 57b0122f..937e29c0 100644 --- a/app/src/components/TopOfBookGraph.tsx +++ b/app/src/components/TopOfBookGraph.tsx @@ -50,7 +50,7 @@ class TopOfBookGraph extends Component { handlePanAndZoom(graphStartTime, graphEndTime); } - }, 200); + }, 100); componentDidMount() { this.chartCanvasRef.subscribe('chartCanvasEvents', { listener: this.handleEvents }); From ab38cc6a83b916ef835715bb9c9151d6c46df242 Mon Sep 17 00:00:00 2001 From: panoreak Date: Sun, 9 Feb 2020 20:54:39 -0500 Subject: [PATCH 6/7] [#157] Added test for loading new data for pan/zoom --- .../OrderBookSnapshot.test.tsx | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/app/src/tests/component-tests/OrderBookSnapshot.test.tsx b/app/src/tests/component-tests/OrderBookSnapshot.test.tsx index 0672540a..c2eabed5 100644 --- a/app/src/tests/component-tests/OrderBookSnapshot.test.tsx +++ b/app/src/tests/component-tests/OrderBookSnapshot.test.tsx @@ -210,3 +210,32 @@ describe('updating price level by message offset functionality', () => { .toEqual(ORDER_BOOK_LIST_ITEMS[135.67]); }); }); + +describe('graph zoom and pan data loading functionality', () => { + let mount, shallow; + + const getTopOfBookOverTimeSpy = jest.spyOn(OrderBookService, 'getTopOfBookOverTime') + .mockImplementation((instrument, startTime, endTime, nDataPoints): Promise => Promise.resolve( + { + data: [], + }, + )); + + beforeEach(() => { + mount = createMount(); + shallow = createShallow({ dive: true }); + }); + + afterEach(() => { + getTopOfBookOverTimeSpy.mockClear(); + mount.cleanUp(); + }); + + it('makes database call when there is a zoom or pan event', () => { + const wrapper = shallow(); + + expect(getTopOfBookOverTimeSpy).toHaveBeenCalledTimes(0); + wrapper.instance().handlePanAndZoom(TIMESTAMP_PM, TIMESTAMP_PM); + expect(getTopOfBookOverTimeSpy).toHaveBeenCalledTimes(1); + }); +}); From bb2e7e5405ae0287c7cdc153cd4d9d1925f8ea72 Mon Sep 17 00:00:00 2001 From: Alvyn Duy-Khoi Le Date: Thu, 13 Feb 2020 21:06:33 -0500 Subject: [PATCH 7/7] [#157] Added logic for nanosecond-precision zooming and panning - Used continuous scale with nanoseconds since the start of day - Showed appropriate nanoseconds labels - Integrated with async calls once more zoomed in --- app/.eslintrc.json | 10 ++ app/src/components/TopOfBookGraph.tsx | 93 +++++++++------ app/src/components/TopOfBookGraphWrapper.tsx | 48 +++++++- app/src/constants/Constants.ts | 16 ++- app/src/models/OrderBook.ts | 10 +- app/src/utils/date-utils.ts | 119 +++++++++++++++++-- app/src/utils/number-utils.ts | 10 ++ package-lock.json | 0 8 files changed, 250 insertions(+), 56 deletions(-) create mode 100644 package-lock.json diff --git a/app/.eslintrc.json b/app/.eslintrc.json index 5bc82ebb..0a40f239 100644 --- a/app/.eslintrc.json +++ b/app/.eslintrc.json @@ -24,6 +24,16 @@ "arrow-body-style": "off", "array-callback-return": "off", "import/prefer-default-export": "off", + "import/extensions": [ + "error", + "ignorePackages", + { + "js": "never", + "jsx": "never", + "ts": "never", + "tsx": "never" + } + ], "no-unused-vars": ["error", { "vars": "all", "args": "none"}], "no-unused-expressions": "off", "no-loop-func": "off", diff --git a/app/src/components/TopOfBookGraph.tsx b/app/src/components/TopOfBookGraph.tsx index 937e29c0..0be9afc7 100644 --- a/app/src/components/TopOfBookGraph.tsx +++ b/app/src/components/TopOfBookGraph.tsx @@ -1,13 +1,12 @@ import React, { Component } from 'react'; import { format } from 'd3-format'; -import { scaleTime } from 'd3-scale'; +import { scaleLinear } from 'd3-scale'; import { LineSeries, StraightLine } from 'react-stockcharts/lib/series'; import { ChartCanvas, Chart } from 'react-stockcharts'; import { ClickCallback } from 'react-stockcharts/lib/interactive'; import { XAxis, YAxis } from 'react-stockcharts/lib/axes'; -import { discontinuousTimeScaleProvider } from 'react-stockcharts/lib/scale'; import { SingleValueTooltip, } from 'react-stockcharts/lib/tooltip'; @@ -19,7 +18,12 @@ import { import bigInt from 'big-integer'; import { debounce } from 'lodash'; -import { getDateObjectForGraphScale, getLocalTimeString, getTimestampForBackend } from '../utils/date-utils'; +import NanoDate from 'nano-date'; +import { + adaptCurrentDateTimezoneToTrueNanoseconds, adaptTrueNanosecondsTimeToCurrentDateTimezone, + buildTimeInTheDayStringFromNanoDate, + getNanoDateFromNsSinceSod, getNsSinceSod, +} from '../utils/date-utils'; import { Colors } from '../styles/App'; import { TopOfBookItem } from '../models/OrderBook'; @@ -32,18 +36,27 @@ interface Props { endOfDay: bigInt.BigInteger, topOfBookItems: Array, handlePanAndZoom: (graphStartTime: bigInt.BigInteger, graphEndTime: bigInt.BigInteger) => void, + sodNanoDate: NanoDate, } class TopOfBookGraph extends Component { private chartCanvasRef: any; + /** + * @function handleEvents + * @description Asynchronous behaviour for user interaction (pan and zoom) with the graph + */ handleEvents = debounce((type, moreProps) => { + const { sodNanoDate } = this.props; if (type === 'panend' || type === 'zoom') { const { handlePanAndZoom, startOfDay, endOfDay } = this.props; + const graphDomain: Array = moreProps.xScale.domain(); + const leftBoundNano: NanoDate = getNanoDateFromNsSinceSod(graphDomain[0], sodNanoDate); + const rightBoundNano: NanoDate = getNanoDateFromNsSinceSod(graphDomain[1], sodNanoDate); + const graphNanoDateDomain: Array = [leftBoundNano, rightBoundNano]; - const graphDomain = moreProps.xScale.domain(); - let graphStartTime = getTimestampForBackend(graphDomain[0]); - let graphEndTime = getTimestampForBackend(graphDomain[1]); + let graphStartTime: bigInt.BigInteger = adaptCurrentDateTimezoneToTrueNanoseconds(graphNanoDateDomain[0]); + let graphEndTime: bigInt.BigInteger = adaptCurrentDateTimezoneToTrueNanoseconds(graphNanoDateDomain[1]); graphStartTime = graphStartTime.lesser(startOfDay) ? startOfDay : graphStartTime; graphEndTime = graphEndTime.greater(endOfDay) ? endOfDay : graphEndTime; @@ -62,19 +75,17 @@ class TopOfBookGraph extends Component { render() { const { - width, height, onTimeSelect, selectedDateTimeNano, topOfBookItems, + width, height, onTimeSelect, topOfBookItems, sodNanoDate, selectedDateTimeNano, } = this.props; - topOfBookItems.forEach(element => { - // @ts-ignore - // eslint-disable-next-line no-param-reassign - element.date = getDateObjectForGraphScale(bigInt(element.timestamp)); - }); - - const xScaleProvider = discontinuousTimeScaleProvider - .inputDateAccessor(d => d.date); - const { - data, - } = xScaleProvider(topOfBookItems); + + const xAccessor = (tobItem: TopOfBookItem) => tobItem.nsSinceStartOfDay; + const xScale = scaleLinear() + .domain([topOfBookItems[0].nsSinceStartOfDay, topOfBookItems[topOfBookItems.length - 1].nsSinceStartOfDay]) + .range([0, topOfBookItems.length - 1]); + const nanoDateForSelection: NanoDate = new NanoDate( + adaptTrueNanosecondsTimeToCurrentDateTimezone(selectedDateTimeNano).toString(), + ); + const nanoSinceSodForSelection: number = getNsSinceSod(nanoDateForSelection); return ( { ratio={width / height} seriesName={'topOfBook'} pointsPerPxThreshold={1} - data={data} + data={topOfBookItems} type={'svg'} - displayXAccessor={d => d.timestamp} - xAccessor={d => d.date} - xScale={scaleTime()} - xExtents={[data[0].date, data[data.length - 1].date]} + xAccessor={xAccessor} + displayXAccessor={a => a.date} + xScale={xScale} + xExtents={[ + topOfBookItems[0].nsSinceStartOfDay, + topOfBookItems[topOfBookItems.length - 1].nsSinceStartOfDay, + ]} > { axisAt={'bottom'} orient={'bottom'} ticks={6} + tickFormat={(nsSinceStartOfDay: number) => { + const recreatedNanoDate: NanoDate = getNanoDateFromNsSinceSod( + nsSinceStartOfDay, sodNanoDate, + ); + + return buildTimeInTheDayStringFromNanoDate(recreatedNanoDate); + }} /> + { + return buildTimeInTheDayStringFromNanoDate(nanoDate); + }} + rectWidth={170} + /> + d.best_bid} stroke={Colors.green} @@ -114,17 +148,6 @@ class TopOfBookGraph extends Component { stroke={Colors.red} strokeWidth={1} /> - - d.best_ask} @@ -146,7 +169,7 @@ class TopOfBookGraph extends Component { type={'vertical'} stroke={Colors.lightBlue} strokeWidth={2} - xValue={getDateObjectForGraphScale(selectedDateTimeNano)} + xValue={nanoSinceSodForSelection} /> diff --git a/app/src/components/TopOfBookGraphWrapper.tsx b/app/src/components/TopOfBookGraphWrapper.tsx index 9cfcf660..e3e2e422 100644 --- a/app/src/components/TopOfBookGraphWrapper.tsx +++ b/app/src/components/TopOfBookGraphWrapper.tsx @@ -1,9 +1,15 @@ import React, { Component } from 'react'; import bigInt from 'big-integer'; import { WithStyles, createStyles, withStyles } from '@material-ui/core/styles'; +import NanoDate from 'nano-date'; import TopOfBookGraph from './TopOfBookGraph'; -import { TopOfBookItem } from '../models/OrderBook'; +import { TopOfBookItem, TopOfBookPackage } from '../models/OrderBook'; import { Styles } from '../styles/TopOfBookGraphWrapper'; +import { + adaptTrueNanosecondsTimeToCurrentDateTimezone, + getNsSinceSod, + getSodNanoDate, +} from '../utils/date-utils'; const styles = createStyles(Styles); @@ -51,6 +57,39 @@ class TopOfBookGraphWrapper extends Component { window.removeEventListener('resize', this.updateDimensions); } + /** + * @desc Prepares the structure sent down to graph + * @private + */ + private prepareTobPackage = (): TopOfBookPackage => { + const { topOfBookItems } = this.props; + let sodNanoDate: NanoDate = new NanoDate(); + + const adaptedTopOfBookItems = topOfBookItems.map((topOfBookItem: TopOfBookItem) => { + const bigIntegerTimestamp: bigInt.BigInteger = adaptTrueNanosecondsTimeToCurrentDateTimezone( + bigInt(topOfBookItem.timestamp), + ); + const exact: NanoDate = new NanoDate(bigIntegerTimestamp.toString()); + const nsSinceStartOfDay: number = getNsSinceSod(exact); + + sodNanoDate = getSodNanoDate(exact); + return { + ...topOfBookItem, + date: exact, + nsSinceStartOfDay, + }; + }); + + return { + topOfBookItems: adaptedTopOfBookItems, + sodNanoDate, + }; + }; + + /** + * @function updateDimensions + * @desc Updates state following a resize of the window + */ updateDimensions = () => { if (this.graphContainerRef.current) { this.setState({ @@ -62,9 +101,11 @@ class TopOfBookGraphWrapper extends Component { render() { const { - classes, onTimeSelect, selectedDateTimeNano, topOfBookItems, handlePanAndZoom, startOfDay, endOfDay, + classes, onTimeSelect, selectedDateTimeNano, handlePanAndZoom, startOfDay, endOfDay, } = this.props; const { graphWidth, graphHeight } = this.state; + const topOfBookPackage: TopOfBookPackage = this.prepareTobPackage(); + return (
{ selectedDateTimeNano={selectedDateTimeNano} startOfDay={startOfDay} endOfDay={endOfDay} - topOfBookItems={topOfBookItems} + topOfBookItems={topOfBookPackage.topOfBookItems} + sodNanoDate={topOfBookPackage.sodNanoDate} handlePanAndZoom={handlePanAndZoom} /> )} diff --git a/app/src/constants/Constants.ts b/app/src/constants/Constants.ts index 8feabd6b..f6eb1fba 100644 --- a/app/src/constants/Constants.ts +++ b/app/src/constants/Constants.ts @@ -15,10 +15,16 @@ export const TILDE_KEY_CODE = 192; export const MESSAGE_LIST_DEFAULT_PAGE_SIZE = 20; -export const NANOSECONDS_IN_NINE_AND_A_HALF_HOURS = bigInt(9.5 * 60 * 60 * 10 ** 9); -export const NANOSECONDS_IN_SIXTEEN_HOURS = bigInt(16 * 60 * 60 * 10 ** 9); -export const NANOSECONDS_IN_ONE_SECOND = 1000000000; -export const NANOSECONDS_IN_ONE_DAY = 86400000000000; -export const NANOSECONDS_IN_ONE_MILLISECOND = 1000000; + +export const NANOSECONDS_IN_ONE_MICROSECOND = 10 ** 3; +export const NANOSECONDS_IN_ONE_MILLISECOND = NANOSECONDS_IN_ONE_MICROSECOND * 10 ** 3; +export const NANOSECONDS_IN_ONE_SECOND = NANOSECONDS_IN_ONE_MILLISECOND * 10 ** 3; +export const NANOSECONDS_IN_ONE_MINUTE = NANOSECONDS_IN_ONE_SECOND * 60; +export const NANOSECONDS_IN_ONE_HOUR = NANOSECONDS_IN_ONE_MINUTE * 60; + +export const NANOSECONDS_IN_NINE_AND_A_HALF_HOURS = bigInt(9.5 * NANOSECONDS_IN_ONE_HOUR); +export const NANOSECONDS_IN_SIXTEEN_HOURS = bigInt(16 * NANOSECONDS_IN_ONE_HOUR); + +export const NANOSECONDS_IN_ONE_DAY = NANOSECONDS_IN_ONE_HOUR * 24; export const NUM_DATA_POINTS_RATIO = 0.5; diff --git a/app/src/models/OrderBook.ts b/app/src/models/OrderBook.ts index 34fb00d7..143bd559 100644 --- a/app/src/models/OrderBook.ts +++ b/app/src/models/OrderBook.ts @@ -1,4 +1,6 @@ /* eslint-disable camelcase */ +import NanoDate from 'nano-date'; + export enum TransactionType { Ask, Bid } export interface Order { @@ -45,8 +47,14 @@ export interface Message { } export interface TopOfBookItem { - date?: Date, + date?: NanoDate, + nsSinceStartOfDay: number, best_ask: number, best_bid: number, timestamp: string, } + +export interface TopOfBookPackage { + topOfBookItems: Array, + sodNanoDate: NanoDate +} diff --git a/app/src/utils/date-utils.ts b/app/src/utils/date-utils.ts index e6734167..641424c9 100644 --- a/app/src/utils/date-utils.ts +++ b/app/src/utils/date-utils.ts @@ -2,10 +2,12 @@ import NanoDate from 'nano-date'; import moment from 'moment'; import bigInt from 'big-integer'; +import { zeroLeftPad } from './number-utils'; + import { NANOSECONDS_IN_ONE_SECOND, NANOSECONDS_IN_ONE_DAY, - NANOSECONDS_IN_ONE_MILLISECOND, + NANOSECONDS_IN_ONE_MILLISECOND, NANOSECONDS_IN_ONE_HOUR, NANOSECONDS_IN_ONE_MINUTE, NANOSECONDS_IN_ONE_MICROSECOND, } from '../constants/Constants'; const EDT_TIMEZONE_OFFSET_IN_MINUTES = 240; @@ -110,25 +112,118 @@ export const getLocalTimeString = (nanosecondTimestamp: string) : string => { /** - * @desc Given a nanosecond timestamp from the back-end, returns a Date object with correct timezone - * @param nanosecondTimestamp {bigInt} - * @return Date + * @desc Given a nanosecond timstamp as a bigInt, returns the bigInt with correct offset based on Date's timezone + * @param nanosecondTimestamp + * @return bigInt */ -export const getDateObjectForGraphScale = (nanosecondTimestamp: bigInt.BigInteger) => { +export const adaptTrueNanosecondsTimeToCurrentDateTimezone = (nanosecondTimestamp: bigInt.BigInteger) => { const localTimezoneDate = new NanoDate(); const localTimezoneOffsetInNano = localTimezoneDate.getTimezoneOffset(); - return new NanoDate((nanosecondTimestamp - .plus((localTimezoneOffsetInNano - EDT_TIMEZONE_OFFSET_IN_MINUTES) * 60 * 10 ** 9)).toString()); + return nanosecondTimestamp.plus( + (localTimezoneOffsetInNano - EDT_TIMEZONE_OFFSET_IN_MINUTES) * NANOSECONDS_IN_ONE_MINUTE, + ); }; + /** - * @desc Given a Date object from the graph's x-axis, returns a nanosecond timestamp with correct back-end timezone - * @param graphDate {bigInt} + * @desc Given a NanoDate object from the graph's x-axis, returns a nanosecond timestamp with correct back-end timezone + * @param graphDate NanoDate * @return bigInt */ -export const getTimestampForBackend = (graphDate: Date) => { +export const adaptCurrentDateTimezoneToTrueNanoseconds = (graphDate: NanoDate) => { const localTimezoneDate = new NanoDate(); const localTimezoneOffsetInNano = localTimezoneDate.getTimezoneOffset(); - return bigInt((graphDate.getTime() * NANOSECONDS_IN_ONE_MILLISECOND)) - .minus((localTimezoneOffsetInNano - EDT_TIMEZONE_OFFSET_IN_MINUTES) * 60 * 10 ** 9); + return bigInt(graphDate.getTime()) + .minus((localTimezoneOffsetInNano - EDT_TIMEZONE_OFFSET_IN_MINUTES) * NANOSECONDS_IN_ONE_MINUTE); +}; + + +/** + * @desc Given a full blown NanoDate object, returns the nanoseconds since the start of that day + * @param exact {NanoDate} + * @return {number} integer + */ +export const getNsSinceSod = (exact: NanoDate): number => { + const nanoHours: number = exact.getHours() * NANOSECONDS_IN_ONE_HOUR; + const nanoMins: number = exact.getMinutes() * NANOSECONDS_IN_ONE_MINUTE; + const nanoSecs: number = exact.getSeconds() * NANOSECONDS_IN_ONE_SECOND; + const nanoMillis: number = exact.getMilliseconds() * NANOSECONDS_IN_ONE_MILLISECOND; + const nanoMicros: number = exact.getMicroseconds() * NANOSECONDS_IN_ONE_MICROSECOND; + const nanos: number = exact.getNanoseconds(); + + return nanoHours + nanoMins + nanoSecs + nanoMillis + nanoMicros + nanos; +}; + + +/** + * @desc Given a NanoDate object, returns a new NanoDate corresponding to the start of that day + * @param nanoDate {NanoDate} + */ +export const getSodNanoDate = (nanoDate: NanoDate) => { + const sodNanoDate: NanoDate = new NanoDate(nanoDate); + + sodNanoDate.setHours(0); + sodNanoDate.setMinutes(0); + sodNanoDate.setSeconds(0); + sodNanoDate.setMilliseconds(0); + sodNanoDate.setMicroseconds(0); + sodNanoDate.setNanoseconds(0); + + return sodNanoDate; +}; + + +/** + * @desc Given an integer representing nanoseconds since start of the day and the NanoDate object + * for that start of day, returns exact NanoDate + * @param nsSinceSod {number} + * @param sodNanoDate {NanoDate} + */ +export const getNanoDateFromNsSinceSod = (nsSinceSod: number, sodNanoDate: NanoDate) => { + let nsSinceSodAgg: number = nsSinceSod; + + const hours: number = Math.floor(nsSinceSodAgg / NANOSECONDS_IN_ONE_HOUR); + nsSinceSodAgg -= (hours * NANOSECONDS_IN_ONE_HOUR); + + const mins: number = Math.floor(nsSinceSodAgg / NANOSECONDS_IN_ONE_MINUTE); + nsSinceSodAgg -= (mins * NANOSECONDS_IN_ONE_MINUTE); + + const secs: number = Math.floor(nsSinceSodAgg / NANOSECONDS_IN_ONE_SECOND); + nsSinceSodAgg -= (secs * NANOSECONDS_IN_ONE_SECOND); + + const millis: number = Math.floor(nsSinceSodAgg / NANOSECONDS_IN_ONE_MILLISECOND); + nsSinceSodAgg -= (millis * NANOSECONDS_IN_ONE_MILLISECOND); + + const micros: number = Math.floor(nsSinceSodAgg / NANOSECONDS_IN_ONE_MICROSECOND); + nsSinceSodAgg -= (micros * NANOSECONDS_IN_ONE_MICROSECOND); + + const nanos: number = Math.floor(nsSinceSodAgg); + + const nanoDate = new NanoDate(sodNanoDate); + nanoDate.setHours(hours); + nanoDate.setMinutes(mins); + nanoDate.setSeconds(secs); + nanoDate.setMilliseconds(millis); + nanoDate.setMicroseconds(micros); + nanoDate.setNanoseconds(nanos); + + return nanoDate; +}; + + +/** + * @desc Given a NanoDate object, recreates a string representation of format HH:mm:SS:llluuunnn + * @param nanoDate + */ +export const buildTimeInTheDayStringFromNanoDate = (nanoDate: NanoDate): string => { + return '' + .concat(zeroLeftPad(nanoDate.getHours(), 2)) + .concat(':') + .concat(zeroLeftPad(nanoDate.getMinutes(), 2)) + .concat(':') + .concat(zeroLeftPad(nanoDate.getSeconds(), 2)) + .concat(':') + .concat(zeroLeftPad(nanoDate.getMilliseconds(), 3)) + .concat(zeroLeftPad(nanoDate.getMicroseconds(), 3)) + .concat(zeroLeftPad(nanoDate.getNanoseconds(), 3)); }; diff --git a/app/src/utils/number-utils.ts b/app/src/utils/number-utils.ts index a051bf44..0dfdec92 100644 --- a/app/src/utils/number-utils.ts +++ b/app/src/utils/number-utils.ts @@ -7,3 +7,13 @@ export const roundNumber = (number: number, decimals: number) => { return Number(number).toFixed(decimals); }; + +/** + * @desc Given a number, pads with zeroes based on a decimal precision + * @param num + * @param places + */ +export const zeroLeftPad = (num: number, places: number) => { + const zero = places - num.toString().length + 1; + return Array(+(zero > 0 && zero)).join('0') + num; +}; diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 00000000..e69de29b