Tutorial: Building Live Candlestick Charts with Next.js and Lightweight-chart

Tutorial: Building Live Candlestick Charts with Next.js and Lightweight-chart

In this tutorial, we'll create a dynamic, real-time candlestick chart that visualizes cryptocurrency price data. We'll leverage the power of Next.js for our frontend framework, Lightweight Charts for beautiful and interactive charting, and the InsightSentry API for streaming live market data via WebSockets.

Prerequisites:

  • Basic understanding of React and Next.js.

  • Node.js and npm (or yarn) installed on your system.

  • An API key from InsightSentry (https://insightsentry.com/).

Project Setup:

  1. Create a Next.js App:

     npx create-next-app@latest my-crypto-chart
     cd my-crypto-chart
    
  2. Install Dependencies:

     npm install lightweight-charts
    

Code Structure:

We'll create a single component for our chart within the app directory (or pages directory if you are not using the app router). Let's name it Chart.tsx (or Chart.jsx if you prefer JavaScript).

Chart Component (Chart.tsx):

"use client";

import { createChart, ChartOptions, DeepPartial, UTCTimestamp } from "lightweight-charts";
import { useCallback, useEffect, useRef } from "react";

// Replace with your actual InsightSentry API key
const wsapikey = "YOUR_INSIGHTSENTRY_API_KEY";

// Helper functions for time formatting
const formatTimeToHHMMSS = (timestamp: UTCTimestamp) => {
    const date = new Date(timestamp * 1000); // Convert to milliseconds
    return date.toLocaleTimeString('en-US', { hour12: false }); // Convert to 24-hour hh:mm:ss format
};
const formatDateToLocalTime = (timestamp: UTCTimestamp) => {
    const date = new Date(timestamp * 1000); // Convert to milliseconds
    return date.toLocaleString(); // Convert to local time string
};

// Chart options (customization)
const chartOptions: DeepPartial<ChartOptions> = {
    layout: {
        textColor: '#1e293b',
        background: { color: 'white' },
        attributionLogo: false
    },
    grid: {
        vertLines: {
            visible: false,
        },
        horzLines: {
            visible: false,
        },
    },
    autoSize: true,
    watermark: {
        visible: true,
        color: "rgba(0, 0, 0, 0.05)",
        text: "InsightSentry",
    },

    timeScale: {
        visible: true,
        timeVisible: true,
        ticksVisible: true,

        tickMarkFormatter: formatTimeToHHMMSS,
        borderColor: "#D1D5DB",
    },

    localization: {
        timeFormatter: formatDateToLocalTime,

    },
    rightPriceScale: {
        borderColor: "#e2e8f0",
    },

    crosshair: {
        horzLine: {
            visible: true,
            labelVisible: true,
            color: 'rgba(59, 130, 246, 0.5)', // Semi-transparent blue
        },
        vertLine: {
            visible: true,
            labelVisible: true,
            color: 'rgba(59, 130, 246, 0.5)', // Semi-transparent blue
        },
    },

};

export default function Chart() {
    const chartContainerRef = useRef<HTMLDivElement>(null);
    const wsRef = useRef<WebSocket | null>(null);
    const chartRef = useRef<any>(null);
    const seriesRef = useRef<any>(null);

    // WebSocket connection function (memoized with useCallback)
    const connectWebSocket = useCallback(() => {
        if (wsRef.current && wsRef.current.readyState === WebSocket.OPEN) {
            console.log("WebSocket already connected.");
            return;
        }
        const ws = new WebSocket('wss://data.insightsentry.com/live');
        ws.onopen = () => {
            const subMsg = JSON.stringify({
                api_key: wsapikey,
                subscriptions: [
                    { code: 'BINANCE:BTCUSDT', bar_type: 'second', bar_interval: 1, type: 'series' }
                ]
            });
            ws.send(subMsg);
        };
        ws.onerror = () => {
            console.error('WebSocket error');
        }
        ws.onmessage = (event) => {
            try {
                const data = JSON.parse(event.data)
                if (!data.code) {
                    return
                }

                if (data.series && seriesRef.current) {
                    const formattedSeries = data.series.map(
                        (item: {
                            time: number
                            open: number
                            high: number
                            low: number
                            close: number
                            volume: number
                        }) => ({
                            time: item.time,
                            open: item.open,
                            high: item.high,
                            low: item.low,
                            close: item.close,
                            volume: item.volume
                        })
                    )
                    for (const item of formattedSeries) {
                        seriesRef.current.update(item)
                    }
                }
            } catch (error) {
                console.error(error)
            }
        }

        ws.onclose = () => {
            console.log('WebSocket closed');
            setTimeout(() => {
                connectWebSocket();
            }, 2000);
        };

        wsRef.current = ws;
    }, []);

    // useEffect for chart creation and WebSocket connection
    useEffect(() => {
        chartRef.current = createChart(chartContainerRef.current, chartOptions);
        seriesRef.current = chartRef.current.addCandlestickSeries({
            priceFormat: {
                type: "price",
                minMove: 0.001
            },
            upColor: "#10B981",
            downColor: "#EF4444",
            borderVisible: false,
            wickUpColor: "#10B981",
            wickDownColor: "#EF4444",
        });
        seriesRef.current.setData([]);
        chartRef.current.timeScale().fitContent();

        connectWebSocket();

        return () => {
            if (wsRef.current) {
                wsRef.current.close();
                wsRef.current = null;
            }
            if (chartRef.current) {
                chartRef.current.remove();
            }
        };
    }, [connectWebSocket]);

    return (
        <div ref={chartContainerRef} className="w-full h-[500px]" />
    );
}

Explanation:

  1. Imports: Import necessary modules from lightweight-charts and react.

  2. API Key: Replace YOUR_INSIGHTSENTRY_API_KEY with your actual API key.

  3. Helper Functions: formatTimeToHHMMSS and formatDateToLocalTime format the timestamps for the chart's time scale.

  4. Chart Options: The chartOptions object configures the appearance and behavior of the chart (colors, gridlines, watermark, crosshair, etc.).

  5. Refs:

    • chartContainerRef: A ref to the DOM element where the chart will be rendered.

    • wsRef: A ref to store the WebSocket connection.

    • chartRef: A ref to the created chart object.

    • seriesRef: A ref to the candlestick series object.

  6. connectWebSocket Function:

    • Establishes a WebSocket connection to the InsightSentry API.

    • Sends a subscription message to receive real-time data for BINANCE:BTCUSDT (you can change this to other symbols).

    • Handles incoming data (onmessage) and updates the chart's series with the new data points.

    • Implements reconnection logic (onclose) to automatically reconnect if the connection drops.

  7. useEffect Hook:

    • Creates the chart using createChart and attaches it to the chartContainerRef element.

    • Adds a candlestick series to the chart using addCandlestickSeries.

    • Sets initial data to an empty array ([]).

    • Calls connectWebSocket to initiate the WebSocket connection.

    • Cleanup Function: Closes the WebSocket connection and removes the chart when the component unmounts.

  8. JSX: Renders a div element with the chartContainerRef to serve as the container for the chart.

Integrating the Chart Component:

Now, import and use the Chart component in your main page component (e.g., app/page.tsx):

import Chart from './Chart';

export default function Home() {
  return (
    <main>
      <h1>Real-Time Crypto Chart</h1>
      <Chart />
    </main>
  );
}

Running the Application:

Start the Next.js development server:

npm run dev

Open your browser and go to http://localhost:3000 (or the port indicated in your console). You should see a beautiful candlestick chart updating in real-time with data from InsightSentry!

Customization:

  • Chart Options: Explore the extensive options provided by Lightweight Charts to customize the chart's appearance (colors, grid, axes, etc.). Refer to the Lightweight Charts documentation for details.

  • Data Subscription: Modify the subscriptions array in the connectWebSocket function to subscribe to different symbols or data types offered by the InsightSentry API.

  • Error Handling: Add more robust error handling to the onmessage event handler to gracefully handle potential issues with the data received from the API.

  • UI Enhancements: Add UI elements like a symbol selector, time interval controls, or technical indicators to enhance the user experience.

Conclusion:

This tutorial demonstrated how to build a real-time candlestick chart using Next.js, Lightweight Charts, and the InsightSentry API. You can expand upon this foundation to create sophisticated financial dashboards and data visualization tools. Remember to consult the documentation for Lightweight Charts and InsightSentry to explore their full capabilities. Happy charting!