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

Real-time areSeries starts from the end of the chart #1701

Open
Yaroslav-Bulavin opened this issue Sep 24, 2024 · 0 comments
Open

Real-time areSeries starts from the end of the chart #1701

Yaroslav-Bulavin opened this issue Sep 24, 2024 · 0 comments

Comments

@Yaroslav-Bulavin
Copy link

Tech stack: react, centrifuge
Actual behavior: Real-time areSeries starts from the end of the chart.
Expected behavior: Real-time areSeries begins from the start of the chart and moving to the right step by step.
Here's the video:

Screen.Recording.2024-09-24.at.23.48.25.mov

Here's if I use fitLeftEdge: true:

Screen.Recording.2024-09-24.at.23.56.18.mov

Chart component code:

import React, { useEffect, useRef } from 'react';

import { Box } from '@chakra-ui/react';
import {
  ChartOptions,
  ColorType,
  DeepPartial,
  IChartApi,
  ISeriesApi,
  createChart,
  Time,
  IPriceLine,
  UTCTimestamp,
  TickMarkType,
} from 'lightweight-charts';

import { Config } from '@/config';
import { useGetOpenBetsQuery } from '@/services/api.service';
import { useBetStore } from '@/store/hooks/useBetStore.hook';
import { useCoinStore } from '@/store/hooks/useCoinStore.hook';
import { CurrencyType } from '@/types';

import {
  defaultTickMarkFormatter,
  localTimezoneOffset,
} from '@/utils/date.util';

const chartOptions: DeepPartial<ChartOptions> = {
  layout: {
    textColor: '#8c93a9',
    background: { type: ColorType.Solid, color: 'transparent' },
    attributionLogo: false,
  },
  grid: {
    vertLines: {
      color: 'transparent',
    },
    horzLines: {
      color: 'transparent',
    },
  },
  timeScale: {
    barSpacing: 1,
    minBarSpacing: 0.1,
    timeVisible: true,
    secondsVisible: true,
    shiftVisibleRangeOnNewBar: true,
    fixLeftEdge: true,
    tickMarkFormatter: (
      time: Time,
      tickMarkType: TickMarkType,
      locale: string,
    ) => {
      return defaultTickMarkFormatter(
        { timestamp: ((time as UTCTimestamp) - localTimezoneOffset) as Time },
        tickMarkType,
        locale,
      );
    },
  },
  autoSize: false,
  crosshair: {
    mode: 0,
    vertLine: {
      style: 0,
      color: '#8c93a980',
      labelBackgroundColor: '#8c93a9',
    },
    horzLine: {
      style: 0,
      color: '#8c93a980',
      labelBackgroundColor: '#8c93a9',
    },
  },
  kineticScroll: {
    touch: true,
    mouse: true,
  },
  leftPriceScale: {
    visible: false,
  },
  rightPriceScale: {
    visible: true,
  },
};

interface PriceChartProps {}

export const PriceChart: React.FC<PriceChartProps> = () => {
  const chartContainerRef = useRef<HTMLDivElement | null>(null);
  const lastPriceRef = useRef<CurrencyType | undefined>(undefined);
  const newPricesRef = useRef<CurrencyType | undefined>(undefined);
  const chartRef = useRef<IChartApi | undefined>(undefined);
  const { coinPrices, coin, lastCoinPrice } = useCoinStore();
  const { lastClosedBet, openBets } = useBetStore();

  const areaSeriesRef = useRef<ISeriesApi<'Area'> | null>(null);
  const openBetPriceLineRef = useRef<IPriceLine[]>([]);

  useGetOpenBetsQuery();

  // Initialize chart and series on mount
  useEffect(() => {
    chartRef.current = createChart(
      chartContainerRef?.current as any,
      chartOptions,
    );
    const createdAreaSeries = chartRef.current.addAreaSeries({
      lastValueVisible: true,
      priceLineVisible: true,
      priceLineSource: 0,
      priceLineColor: '#a1f06c',
      priceLineStyle: 3,
      lineType: 2,
      lineWidth: 2,
      lineColor: '#f4b243',
      topColor: '#f4b24340',
      bottomColor: '#f4b24300',
      lastPriceAnimation: 1,
      crosshairMarkerVisible: true,
      autoscaleInfoProvider: (original: any) => {
        const res = original();
        if (res !== null) {
          res.priceRange.minValue -= 50;
          res.priceRange.maxValue += 50;
        }
        return res;
      },
    });

    const formattedInitPrices = coinPrices.map((info) => ({
      time: (new Date(info.timestamp).getTime() / 1000) as Time,
      value: Number(info?.price),
    }));

    createdAreaSeries.setData(formattedInitPrices);

    areaSeriesRef.current = createdAreaSeries;
    console.log('Chart created');

    chartRef.current.timeScale().fitContent();

    return () => {
      console.log('Cleaning up chart');
      lastPriceRef.current = undefined;
      newPricesRef.current = undefined;
      chartRef.current?.remove();
      chartRef.current = undefined;
    };
  }, [coin]);

  // Update data when newPrice changes
  useEffect(() => {
    if (coinPrices && coin && coinPrices.length > 1) {
      const lastPrice = coinPrices[coinPrices?.length - 2];
      newPricesRef.current = coinPrices[coinPrices?.length - 1];
      lastPriceRef.current = lastPrice;
    }

    return () => {
      newPricesRef.current = undefined;
      lastPriceRef.current = undefined;
    };
  }, [coinPrices]);

  // Easing function for smooth transition (ease-out effect)
  const easeOutQuad = (t: number) => t * (2 - t);

  const animatePriceUpdate = (
    startTime: number,
    duration: number,
    startPrice: number,
    endPrice: number,
  ) => {
    const currentTime = Date.now();
    const elapsed = currentTime - startTime;
    const progress = Math.min(elapsed / duration, 1);
    const easedProgress = easeOutQuad(progress);
    const interpolatedPrice =
      startPrice + (endPrice - startPrice) * easedProgress;

    // Apply the price update to the chart
    areaSeriesRef.current?.update({
      time: (currentTime / 1000) as UTCTimestamp,
      value: interpolatedPrice,
    });

    if (progress < 1) {
      requestAnimationFrame(() =>
        animatePriceUpdate(
          startTime,
          duration,
          startPrice,
          endPrice,
        ),
      );
    }
  };

  const updatePriceWithAnimation = (
    newPrice: CurrencyType,
    lastPrice: CurrencyType,
  ) => {
    const startTime = Date.now();
    const duration = 500; // animation duration in ms
    const startPrice = Number(lastPrice?.price);
    const endPrice = Number(newPrice?.price);

    if (areaSeriesRef.current) {
      animatePriceUpdate(
        startTime,
        duration,
        startPrice,
        endPrice,
      );
    }
  };

  const updateChart = () => {
    if (newPricesRef?.current && lastPriceRef?.current) {
      const tempNewPrice = newPricesRef.current;
      const tempLastPrice = lastPriceRef.current;

      areaSeriesRef.current?.applyOptions({
        priceLineColor:
          +tempNewPrice?.price >= +tempLastPrice?.price ? '#a1f06c' : '#ec5d51',
      });

      // Animate the price update with the "heartbeat" effect
      updatePriceWithAnimation(tempNewPrice, tempLastPrice);

      const clonedInfo = [...coinPrices];
      clonedInfo.shift();
    }
  };

  useEffect(() => {
    // Set up the interval
    const intervalId = setInterval(updateChart, Config.priceInterval);
    // Clear interval on component unmount
    return () => {
      clearInterval(intervalId);
      if (areaSeriesRef.current) {
        areaSeriesRef.current.setData([]);
      }
    };
  }, []);

  return <Box h='full' ref={chartContainerRef} w='full' />;
};

Method chartRef.current?.timeScale().fitContent(); also doesn't work.

Didn't find how to do it in the docs and google.

Please help! Thanks in advance!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant