Skip to content

Commit

Permalink
Merge pull request #13 from deriv-com/shafin/ChampionTrader/feat-chart
Browse files Browse the repository at this point in the history
chore: update simple chart with real data
  • Loading branch information
shafin-deriv authored Feb 4, 2025
2 parents 63a5f10 + 0255f8a commit a5df84f
Show file tree
Hide file tree
Showing 9 changed files with 208 additions and 119 deletions.
1 change: 1 addition & 0 deletions STRUCTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ src/
├── components/ # Reusable UI components
│ ├── BalanceDisplay/ # Displays the user balance.
│ ├── BalanceHandler/ # Manages balance state.
│ ├── Chart/ # Displays market data using WebSocket integration.
│ └── ContractSSEHandler/ # Handles contract SSE streaming.
├── hooks/ # Custom React hooks
│ ├── sse/ # SSE hooks for real-time data
Expand Down
14 changes: 14 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"dotenv": "^16.4.7",
"lightweight-charts": "^5.0.1",
"lucide-react": "^0.316.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
Expand Down
74 changes: 0 additions & 74 deletions src/App.test.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { render, act, screen } from "@testing-library/react";
import { App } from "./App";
import { useMarketWebSocket } from "@/hooks/websocket";
import { useContractSSE } from "@/hooks/sse";
import { MainLayout } from "@/layouts/MainLayout";
import { useClientStore } from "@/stores/clientStore";
Expand Down Expand Up @@ -42,9 +41,6 @@ jest.mock("@/layouts/MainLayout", () => ({
}));

// Mock the websocket, SSE hooks, and client store
jest.mock("@/hooks/websocket", () => ({
useMarketWebSocket: jest.fn(),
}));

jest.mock("@/hooks/sse", () => ({
useContractSSE: jest.fn(),
Expand All @@ -62,7 +58,6 @@ jest.mock("@/stores/clientStore", () => ({

describe("App", () => {
const mockMainLayout = MainLayout as jest.Mock;
const mockUseMarketWebSocket = useMarketWebSocket as jest.Mock;
const mockUseContractSSE = useContractSSE as jest.Mock;
const mockUseClientStore = useClientStore as unknown as jest.Mock;

Expand All @@ -79,11 +74,6 @@ describe("App", () => {
logout: jest.fn(),
});

mockUseMarketWebSocket.mockReturnValue({
isConnected: true,
error: null,
price: null,
});

mockUseContractSSE.mockReturnValue({
price: null,
Expand All @@ -105,73 +95,9 @@ describe("App", () => {
expect(await screen.findByTestId("trade-page")).toBeInTheDocument();
});

it("initializes market websocket with correct instrument", () => {
render(<App />);

// Verify MainLayout is rendered
expect(screen.getByTestId("main-layout")).toBeInTheDocument();

expect(mockUseMarketWebSocket).toHaveBeenCalledWith(
"R_100",
expect.objectContaining({
onConnect: expect.any(Function),
onError: expect.any(Function),
onPrice: expect.any(Function),
})
);
});

it("logs connection status changes", () => {
mockUseMarketWebSocket.mockReturnValue({
isConnected: false,
error: null,
price: null,
});

render(<App />);

expect(console.log).toHaveBeenCalledWith("Market WebSocket Disconnected");
});

it("handles websocket errors", () => {
const mockError = new Error("WebSocket error");

render(<App />);

// Get the error handler from the mock calls
const { onError } = mockUseMarketWebSocket.mock.calls[0][1];

// Simulate error
act(() => {
onError(mockError);
});

expect(console.log).toHaveBeenCalledWith(
"Market WebSocket Error:",
mockError
);
});

it("handles price updates", () => {
render(<App />);

const mockPrice = {
instrument_id: "R_100",
bid: 100,
ask: 101,
timestamp: "2024-01-30T00:00:00Z",
};

// Get the price handler from the mock calls
const { onPrice } = mockUseMarketWebSocket.mock.calls[0][1];

// Simulate price update
act(() => {
onPrice(mockPrice);
});

expect(console.log).toHaveBeenCalledWith("Price Update:", mockPrice);
});

it("handles contract SSE price updates when logged in", () => {
mockUseClientStore.mockReturnValue({
Expand Down
13 changes: 0 additions & 13 deletions src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { lazy, Suspense, useEffect, useState } from "react";
import { BrowserRouter, Routes, Route, Navigate } from "react-router-dom";
import { MainLayout } from "@/layouts/MainLayout";
import { useMarketWebSocket } from "@/hooks/websocket";
import { useClientStore } from "@/stores/clientStore";
import { ContractSSEHandler } from "@/components/ContractSSEHandler";
import { BalanceHandler } from "@/components/BalanceHandler";
Expand All @@ -21,21 +20,9 @@ const MenuPage = lazy(() =>
);

const AppContent = () => {
// Initialize market websocket for default instrument
const { isConnected } = useMarketWebSocket("R_100", {
onConnect: () => console.log("Market WebSocket Connected"),
onError: (error) => console.log("Market WebSocket Error:", error),
onPrice: (price) => console.log("Price Update:", price),
});

const { token, isLoggedIn } = useClientStore();

// Log connection status changes
useEffect(() => {
if (!isConnected) {
console.log("Market WebSocket Disconnected");
}
}, [isConnected]);

return (
<MainLayout>
Expand Down
157 changes: 152 additions & 5 deletions src/components/Chart/Chart.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,162 @@
import React from 'react';
import { cn } from '@/lib/utils';
import React, { useEffect, useRef, useState } from "react";
import { cn } from "@/lib/utils";
import { useMarketWebSocket } from "@/hooks/websocket";
import {
createChart,
IChartApi,
UTCTimestamp,
SingleValueData,
BaselineSeries,
} from "lightweight-charts";

interface ChartProps {
className?: string;
}

interface ChartData {
time: UTCTimestamp;
value: number;
}

export const Chart: React.FC<ChartProps> = ({ className }) => {
const chartContainerRef = useRef<HTMLDivElement>(null);
const chartRef = useRef<IChartApi | null>(null);
const seriesRef = useRef<any | null>(null);
const [currentPrice, setCurrentPrice] = useState<number | null>(null);
const [currentTime, setCurrentTime] = useState<string | null>(null);
const [priceHistory, setPriceHistory] = useState<ChartData[]>([]);

const { isConnected } = useMarketWebSocket("R_100", {
onConnect: () => console.log("Market WebSocket Connected in Chart"),
onError: (err) => console.log("Market WebSocket Error in Chart:", err),
onPrice: (price) => {
if (price?.ask) {
const timestamp = new Date(price.timestamp);
const newPrice: ChartData = {
time: Math.floor(timestamp.getTime() / 1000) as UTCTimestamp,
value: price.ask,
};
setPriceHistory((prev) => [...prev, newPrice]);
setCurrentPrice(price.ask);
setCurrentTime(timestamp.toLocaleString());

if (seriesRef.current) {
seriesRef.current.update(newPrice);
}
}
},
});

useEffect(() => {
if (!chartContainerRef.current) return;

// Create chart
const chart = createChart(chartContainerRef.current, {
layout: {
background: { color: "white" },
textColor: "black",
},
grid: {
vertLines: { color: "#f0f0f0" },
horzLines: { color: "#f0f0f0" },
},
rightPriceScale: {
borderVisible: false,
},
timeScale: {
borderVisible: false,
timeVisible: true,
secondsVisible: true,
},
crosshair: {
vertLine: {
labelBackgroundColor: "#404040",
},
horzLine: {
labelBackgroundColor: "#404040",
},
},
width: chartContainerRef.current.clientWidth,
height: chartContainerRef.current.clientHeight,
});

const baselineSeries = chart.addSeries(BaselineSeries, {
// baseValue: { type: "price", price: undefined },
topLineColor: "rgba( 38, 166, 154, 1)",
topFillColor1: "rgba( 38, 166, 154, 0.28)",
topFillColor2: "rgba( 38, 166, 154, 0.05)",
bottomLineColor: "rgba( 239, 83, 80, 1)",
bottomFillColor1: "rgba( 239, 83, 80, 0.05)",
bottomFillColor2: "rgba( 239, 83, 80, 0.28)",
});

chartRef.current = chart;
seriesRef.current = baselineSeries;

// Subscribe to crosshair move to update the tooltip
chart.subscribeCrosshairMove((param) => {
if (param.time) {
const data = param.seriesData.get(baselineSeries) as SingleValueData;
if (data?.value) {
setCurrentPrice(data.value);
const timestamp = new Date((param.time as number) * 1000);
setCurrentTime(timestamp.toLocaleString());
}
}
});

// Handle window resize
const handleResize = () => {
if (chartContainerRef.current && chartRef.current) {
chartRef.current.applyOptions({
width: chartContainerRef.current.clientWidth,
height: chartContainerRef.current.clientHeight,
});
}
};

window.addEventListener("resize", handleResize);

// Cleanup
return () => {
window.removeEventListener("resize", handleResize);
if (chartRef.current) {
chartRef.current.remove();
}
};
}, []);

// Update data when price history changes
useEffect(() => {
if (seriesRef.current && priceHistory.length > 0) {
seriesRef.current.setData(priceHistory);
chartRef.current?.timeScale().fitContent();
}
}, [priceHistory]);

return (
<div className={cn("contents", className)}>
<div className="flex-1 bg-gray-100 h-full w-full rounded-lg flex items-center justify-center">
Chart Component
<div className={cn("flex flex-col flex-1", className)}>
<div className="flex-1 bg-white w-full rounded-lg relative min-h-[400px]">
{currentPrice && currentTime && (
<div className="absolute top-4 left-4 bg-gray-100 p-2 rounded shadow-sm z-10">
<div className="text-sm font-medium">VOLATILITY 100 (1S) INDEX</div>
<div className="text-lg font-bold">{currentPrice.toFixed(2)}</div>
<div className="text-xs text-gray-600">{currentTime}</div>
</div>
)}
<div
ref={chartContainerRef}
className="w-full h-full"
data-testid="chart-container"
/>
{!isConnected && (
<div
data-testid="ws-disconnected"
className="text-center text-red-500"
>
Disconnected
</div>
)}
</div>
</div>
);
Expand Down
39 changes: 39 additions & 0 deletions src/components/Chart/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# Chart Component

The Chart component displays market data using a WebSocket connection. It is responsible for:

- **Real-Time Price Updates:** Receiving and displaying live market prices.
- **Connection Status:** Indicating whether the WebSocket connection is active or disconnected.
- **Error Handling:** Displaying errors when the WebSocket encounters issues.
- **Customization:** Accepting an optional `className` prop for styling.

## Usage

```tsx
import { Chart } from "@/components/Chart";

function App() {
return (
<div>
<Chart className="custom-chart-class" />
</div>
);
}
```

## Props

| Prop | Type | Description |
|-----------|--------|---------------------------------------|
| className | string | Optional CSS class for the component. |

## Implementation Details

- The component uses the `useMarketWebSocket` hook with a hardcoded instrument ID of `"R_100"`.
- WebSocket events such as connection, price updates, and errors are logged to the console.
- The UI displays the current market price, a disconnected message when the connection is lost, and any error messages.

## Future Enhancements

- Integrate a visual charting library for dynamic price visualization.
- Add configurable instrument IDs and callback functions via props.
Loading

0 comments on commit a5df84f

Please sign in to comment.