Skip to content

Commit

Permalink
feat: connect chart and depth components by price
Browse files Browse the repository at this point in the history
  • Loading branch information
dib542 committed May 18, 2024
1 parent 129ae48 commit a233b00
Show file tree
Hide file tree
Showing 6 changed files with 253 additions and 19 deletions.
4 changes: 0 additions & 4 deletions src/pages/Orderbook/Orderbook.scss
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,5 @@
border-top-right-radius: 0;
border-bottom-right-radius: 0;
}

.chart-depth-connector {
min-width: paddings.$p-3;
}
}
}
72 changes: 70 additions & 2 deletions src/pages/Orderbook/Orderbook.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@ import TabsCard from '../../components/cards/TabsCard';
import { Tab } from '../../components/Tabs/Tabs';
import OrderbookHeader from './OrderbookHeader';
import OrderbookFooter from './OrderbookFooter';
import OrderBookChart from './OrderbookChart';
import OrderBookChart, { ChartPriceAxisInfo } from './OrderbookChart';
import OrderbookChartConnector from './OrderbookChartConnector';
import OrderBookList from './OrderbookList';
import OrderBookTradesList from './OrderbookTradesList';
import LimitOrderCard from '../../components/cards/LimitOrderCard';
Expand All @@ -33,7 +34,9 @@ function Orderbook() {
const { data: tokenA } = useToken(denomA);
const { data: tokenB } = useToken(denomB);

const [chartPriceAxis, setChartPriceAxis] = useState<ChartPriceAxisInfo>();
const [depthPriceIndication, setDepthPriceIndication] = useState<number>();
const [depthPriceOffset, setDepthPriceOffset] = useState<number>();

return (
<div className="decontainer flex col gap-3">
Expand All @@ -50,10 +53,32 @@ function Orderbook() {
tokenB={tokenA}
priceIndication={depthPriceIndication}
setPriceIndication={setDepthPriceIndication}
setPriceAxis={setChartPriceAxis}
/>
)}
</div>
<div className="chart-depth-connector col"></div>
<div className="chart-depth-connector col">
<OrderbookChartConnector
connectionPoints={useMemo(() => {
if (
chartPriceAxis &&
depthPriceIndication &&
depthPriceOffset
) {
return [
[
getChartPricePointOffset(
chartPriceAxis,
depthPriceIndication
),
depthPriceOffset,
],
];
}
return [];
}, [chartPriceAxis, depthPriceIndication, depthPriceOffset])}
/>
</div>
<div className="col">
<TabsCard
className="flex"
Expand All @@ -73,6 +98,7 @@ function Orderbook() {
tokenB={tokenB}
priceIndication={depthPriceIndication}
setPriceIndication={setDepthPriceIndication}
setPriceOffset={setDepthPriceOffset}
/>
) : null,
},
Expand Down Expand Up @@ -102,3 +128,45 @@ function Orderbook() {
</div>
);
}

function getPointOffsetPercent(
value: number,
max: number,
min: number = 0,
mode: ChartPriceAxisInfo['mode'] = 0
): number {
switch (mode) {
// handle linear math
case 0: {
return (value - min) / (max - min);
}
}
// todo: handle other math
return NaN;
}
function getPointOffset(
percent: number,
extent: number,
offset: number = 0
): number {
return percent * extent + offset;
}

const chartPriceAxisOffset = {
top: 42,
};
function getChartPricePointOffset(
chartPriceAxis: ChartPriceAxisInfo,
price: number
) {
return getPointOffset(
getPointOffsetPercent(
price,
chartPriceAxis.from,
chartPriceAxis.to,
chartPriceAxis.mode
),
chartPriceAxis.height,
chartPriceAxisOffset.top
);
}
54 changes: 53 additions & 1 deletion src/pages/Orderbook/OrderbookChart.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ import {
SearchSymbolResultItem,
Bar,
Timezone,
VisiblePriceRange,
PriceScaleMode,
IChartWidgetApi,
} from 'charting_library';

Expand Down Expand Up @@ -40,7 +42,14 @@ const defaultWidgetOptions: Partial<ChartingLibraryWidgetOptions> = {
container: '',
locale: 'en',
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone as Timezone,
disabled_features: ['header_symbol_search', 'header_compare'],
disabled_features: [
// disable searching for symbols inside chart: token pair search is on page
'header_symbol_search',
// disable comparing multiple symbols: can get complicated
'header_compare',
// disable mouse-down panning behavior: ensures last tick is current time
'chart_scroll',
],
enabled_features: [],
charts_storage_url: 'https://saveload.tradingview.com',
charts_storage_api_version: '1.1',
Expand Down Expand Up @@ -72,15 +81,23 @@ interface BlockRangeRequestQuery extends RequestQuery {

type TimeSeriesResolution = 'second' | 'minute' | 'hour' | 'day' | 'month';

export interface ChartPriceAxisInfo extends VisiblePriceRange {
mode: PriceScaleMode;
height: number;
}
export default function OrderBookChart({
tokenA,
tokenB,
priceIndication,
setPriceAxis,
}: {
tokenA: Token;
tokenB: Token;
priceIndication?: number | undefined;
setPriceIndication?: React.Dispatch<React.SetStateAction<number | undefined>>;
setPriceAxis?: React.Dispatch<
React.SetStateAction<ChartPriceAxisInfo | undefined>
>;
}) {
const tokenIdA = getTokenId(tokenA);
const tokenIdB = getTokenId(tokenB);
Expand Down Expand Up @@ -502,6 +519,41 @@ export default function OrderBookChart({
}
}, [chart, priceIndication]);

useEffect(() => {
if (chart) {
const checkChartAxis = () => {
setPriceAxis?.((chartPriceAxis) => {
const pane = chart?.getPanes()[0];
const rightPriceScale = pane?.getRightPriceScales()[0];
const visiblePriceRange = rightPriceScale?.getVisiblePriceRange();
const newChartPriceAxis: ChartPriceAxisInfo | undefined =
rightPriceScale && visiblePriceRange
? {
from: visiblePriceRange.from,
to: visiblePriceRange.to,
mode: rightPriceScale.getMode(),
height: pane.getHeight(),
}
: undefined;
if (
(chartPriceAxis && !newChartPriceAxis) ||
(!chartPriceAxis && newChartPriceAxis) ||
chartPriceAxis?.from !== newChartPriceAxis?.from ||
chartPriceAxis?.to !== newChartPriceAxis?.to ||
chartPriceAxis?.mode !== newChartPriceAxis?.mode ||
chartPriceAxis?.height !== newChartPriceAxis?.height
) {
return newChartPriceAxis;
}
return chartPriceAxis;
});
timeout = setTimeout(checkChartAxis, 100);
};
let timeout = setTimeout(checkChartAxis, 0);
return () => clearTimeout(timeout);
}
}, [chart, setPriceAxis]);

return <div className="trading-view-chart flex" ref={chartRef}></div>;
}

Expand Down
9 changes: 9 additions & 0 deletions src/pages/Orderbook/OrderbookChartConnector.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
@use '../../styles/mixins-vars/margins.scss' as margins;
@use '../../styles/mixins-vars/paddings.scss' as paddings;

.orderbook-page {
.chart-depth-connector {
min-width: paddings.$p-3;
max-width: 50px;
}
}
71 changes: 71 additions & 0 deletions src/pages/Orderbook/OrderbookChartConnector.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import { useCallback, useState } from 'react';
import useResizeObserver from '@react-hook/resize-observer';

import './OrderbookChartConnector.scss';

type ConnectionPoint = [number, number];

export default function OrderbookChartConnector({
connectionPoints,
}: {
connectionPoints?: ConnectionPoint[];
}) {
// define what to draw
const draw = useCallback(
(canvas: HTMLCanvasElement | null) => {
const ctx = canvas?.getContext('2d');
if (canvas && ctx) {
// reset canvas
canvas.width = canvas.offsetWidth;
canvas.height = canvas.offsetHeight;
const width = canvas.width;
const height = canvas.height;
ctx?.clearRect(0, 0, width, height);
// draw elements
if (connectionPoints?.length) {
drawConnectionPoints(ctx, connectionPoints);
}
}

// define drawing functions
function drawConnectionPoints(
ctx: CanvasRenderingContext2D,
connectionPoints: ConnectionPoint[],
width: number = ctx.canvas.width
) {
ctx.lineWidth = 1;
ctx.lineJoin = 'round';
ctx.strokeStyle = 'white';
ctx.beginPath();
connectionPoints.forEach(([y1, y2]) => {
ctx.moveTo(sharpPoint(0), sharpPoint(y1));
ctx.lineTo(sharpPoint(0.7 * width), sharpPoint(y1));
ctx.lineTo(sharpPoint(0.9 * width), sharpPoint(y2));
ctx.lineTo(sharpPoint(width), sharpPoint(y2));
});
ctx.stroke();
}

// use points centered at half-pixels for sharp lines
function sharpPoint(value: number): number {
return Math.round(value) + 0.5;
}
},
[connectionPoints]
);

// store ref but also draw on canvas when first found
const [canvas, setCanvas] = useState<HTMLCanvasElement | null>(null);
const getCanvasRef = useCallback(
(canvas: HTMLCanvasElement | null) => {
setCanvas(canvas);
draw(canvas);
},
[draw]
);

// redraw canvas when the screen size changes
useResizeObserver(canvas, () => draw(canvas));

return <canvas className="flex" ref={getCanvasRef}></canvas>;
}
Loading

0 comments on commit a233b00

Please sign in to comment.