Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chore: update simple chart with real data #13

Merged
merged 1 commit into from
Feb 4, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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),
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion (bug_risk): Consider implementing proper error handling for WebSocket errors

Instead of just logging to console, consider showing an error state to users and implementing reconnection logic.

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]);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion (performance): Consider implementing a limit on price history to prevent unbounded growth

The price history array will grow indefinitely. Consider implementing a maximum length and removing older entries.

Suggested implementation:

  const MAX_PRICE_HISTORY = 1000; // Keep last 1000 price points
  const { isConnected } = useMarketWebSocket("R_100", {
        setPriceHistory((prev) => {
          const updatedHistory = [...prev, newPrice];
          return updatedHistory.length > MAX_PRICE_HISTORY
            ? updatedHistory.slice(-MAX_PRICE_HISTORY)
            : updatedHistory;
        });

setCurrentPrice(price.ask);
setCurrentTime(timestamp.toLocaleString());

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

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

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion (code-quality): Use block braces for ifs, whiles, etc. (use-braces)

Suggested change
if (!chartContainerRef.current) return;
if (!chartContainerRef.current) {


ExplanationIt is recommended to always use braces and create explicit statement blocks.

Using the allowed syntax to just write a single statement can lead to very confusing
situations, especially where subsequently a developer might add another statement
while forgetting to add the braces (meaning that this wouldn't be included in the condition).


// 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);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion (performance): Consider debouncing the resize handler to improve performance

Frequent resize events could impact performance. Consider using a debounced version of the handler.

Suggested implementation:

    // Import at the top of the file
    import debounce from 'lodash/debounce';

    // Handle window resize with debouncing
    const handleResize = debounce(() => {
    return () => {
      window.removeEventListener("resize", handleResize);
      handleResize.cancel(); // Cancel any pending debounced calls

You'll need to:

  1. Install lodash if not already installed: npm install lodash or yarn add lodash
  2. Optionally adjust the debounce wait time by passing a second argument to debounce if needed, e.g., debounce(() => {...}, 250)


// 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