Skip to content

Commit

Permalink
Merge pull request #186 from Lercerss/graph_zooming_cleaned
Browse files Browse the repository at this point in the history
Graph zooming & panning
  • Loading branch information
alvyn279 authored Feb 24, 2020
2 parents 329665c + bb2e7e5 commit 83935c1
Show file tree
Hide file tree
Showing 10 changed files with 357 additions and 65 deletions.
10 changes: 10 additions & 0 deletions app/.eslintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
50 changes: 38 additions & 12 deletions app/src/components/OrderBookSnapshot.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -49,18 +49,18 @@ interface State {
loadingOrderbook: boolean,
loadingGraph: boolean,
graphUnavailable: boolean,
graphStartTime: bigInt.BigInteger,
graphEndTime: bigInt.BigInteger,
}

class OrderBookSnapshot extends Component<WithStyles, State> {
/**
* @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);

Expand All @@ -82,6 +82,8 @@ class OrderBookSnapshot extends Component<WithStyles, State> {
loadingOrderbook: false,
loadingGraph: false,
graphUnavailable: false,
graphStartTime: bigInt(0),
graphEndTime: bigInt(0),
};
}

Expand Down Expand Up @@ -125,9 +127,16 @@ class OrderBookSnapshot extends Component<WithStyles, State> {
() => {
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);
},
);
}
},
);
Expand Down Expand Up @@ -161,19 +170,21 @@ class OrderBookSnapshot extends Component<WithStyles, State> {
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(
{
datePickerValue: date,
selectedTimeString,
selectedDateNano,
selectedDateTimeNano,
graphStartTime,
graphEndTime,
},
() => {
this.handleChangeDateTime();
this.updateGraphData(startTime, endTime);
this.updateGraphData(graphStartTime, graphEndTime);
},
);
};
Expand Down Expand Up @@ -268,17 +279,19 @@ class OrderBookSnapshot extends Component<WithStyles, State> {
/**
* @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;

OrderBookService.getTopOfBookOverTime(selectedInstrument, startTime.toString(), endTime.toString(),
OrderBookService.getTopOfBookOverTime(selectedInstrument, graphStartTime.toString(), graphEndTime.toString(),
this.getNumDataPoints())
.then(response => {
// eslint-disable-next-line camelcase
const result = response.data;

this.setState(
{
graphStartTime,
graphEndTime,
topOfBookItems: result,
loadingGraph: false,
},
Expand All @@ -297,12 +310,22 @@ class OrderBookSnapshot extends Component<WithStyles, State> {
});
};

/**
* @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.updateGraphData(graphStartTime, graphEndTime);
};

render() {
const { classes } = this.props;
const {
listItems,
maxQuantity,
selectedDateTimeNano,
selectedDateNano,
datePickerValue,
selectedTimeString,
lastSodOffset,
Expand Down Expand Up @@ -451,7 +474,10 @@ class OrderBookSnapshot extends Component<WithStyles, State> {
<TopOfBookGraphWrapper
className={classes.graph}
onTimeSelect={this.handleSelectGraphDateTime}
handlePanAndZoom={this.handlePanAndZoom}
selectedDateTimeNano={selectedDateTimeNano}
startOfDay={selectedDateNano.plus(NANOSECONDS_IN_NINE_AND_A_HALF_HOURS)}
endOfDay={selectedDateNano.plus(NANOSECONDS_IN_SIXTEEN_HOURS)}
topOfBookItems={topOfBookItems}
/>
)}
Expand Down
119 changes: 84 additions & 35 deletions app/src/components/TopOfBookGraph.tsx
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -18,52 +17,93 @@ import {
} from 'react-stockcharts/lib/coordinates';

import bigInt from 'big-integer';
import { getDateObjectForGraphScale, getLocalTimeString } from '../utils/date-utils';
import { debounce } from 'lodash';
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';


interface Props {
height: number,
width: number,
onTimeSelect: (any) => void,
selectedDateTimeNano: bigInt.BigInteger,
startOfDay: bigInt.BigInteger,
endOfDay: bigInt.BigInteger,
topOfBookItems: Array<TopOfBookItem>,
handlePanAndZoom: (graphStartTime: bigInt.BigInteger, graphEndTime: bigInt.BigInteger) => void,
sodNanoDate: NanoDate,
}

class TopOfBookGraph extends Component<Props> {
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<number> = moreProps.xScale.domain();
const leftBoundNano: NanoDate = getNanoDateFromNsSinceSod(graphDomain[0], sodNanoDate);
const rightBoundNano: NanoDate = getNanoDateFromNsSinceSod(graphDomain[1], sodNanoDate);
const graphNanoDateDomain: Array<NanoDate> = [leftBoundNano, rightBoundNano];

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;

handlePanAndZoom(graphStartTime, graphEndTime);
}
}, 100);

componentDidMount() {
this.chartCanvasRef.subscribe('chartCanvasEvents', { listener: this.handleEvents });
}

componentWillUnmount() {
this.chartCanvasRef.unsubscribe('chartCanvasEvents');
}

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 (
<ChartCanvas
ref={node => { this.chartCanvasRef = node; }}
width={width}
height={height}
ratio={width / height}
seriesName={'topOfBook'}
pointsPerPxThreshold={1}
data={data}
data={topOfBookItems}
type={'svg'}
displayXAccessor={d => d.timestamp}
xAccessor={d => d.date}
xScale={scaleTime()}
panEvent={false}
zoomEvent={false}
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,
]}
>
<Chart
id={0}
Expand All @@ -73,11 +113,31 @@ class TopOfBookGraph extends Component<Props> {
axisAt={'bottom'}
orient={'bottom'}
ticks={6}
tickFormat={(nsSinceStartOfDay: number) => {
const recreatedNanoDate: NanoDate = getNanoDateFromNsSinceSod(
nsSinceStartOfDay, sodNanoDate,
);

return buildTimeInTheDayStringFromNanoDate(recreatedNanoDate);
}}
/>
<YAxis
axisAt={'left'}
orient={'left'}
/>
<MouseCoordinateX
at={'bottom'}
orient={'bottom'}
displayFormat={(nanoDate: NanoDate) => {
return buildTimeInTheDayStringFromNanoDate(nanoDate);
}}
rectWidth={170}
/>
<MouseCoordinateY
at={'right'}
orient={'right'}
displayFormat={format('.2f')}
/>
<LineSeries
yAccessor={d => d.best_bid}
stroke={Colors.green}
Expand All @@ -88,17 +148,6 @@ class TopOfBookGraph extends Component<Props> {
stroke={Colors.red}
strokeWidth={1}
/>
<MouseCoordinateX
at={'bottom'}
orient={'bottom'}
displayFormat={getLocalTimeString}
rectWidth={150}
/>
<MouseCoordinateY
at={'right'}
orient={'bottom'}
displayFormat={format('.2f')}
/>
<SingleValueTooltip
yLabel={'Ask'}
yAccessor={d => d.best_ask}
Expand All @@ -120,7 +169,7 @@ class TopOfBookGraph extends Component<Props> {
type={'vertical'}
stroke={Colors.lightBlue}
strokeWidth={2}
xValue={getDateObjectForGraphScale(selectedDateTimeNano)}
xValue={nanoSinceSodForSelection}
/>
</Chart>
<CrossHairCursor />
Expand Down
Loading

0 comments on commit 83935c1

Please sign in to comment.