Skip to content
New issue

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

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

Already on GitHub? # to your account

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

Open
Yaroslav-Bulavin opened this issue Sep 24, 2024 · 1 comment
Open

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

Yaroslav-Bulavin opened this issue Sep 24, 2024 · 1 comment

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!

@safaritrader
Copy link

try work with this :

chart.timeScale().setVisibleLogicalRange({from:0,to:27})

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

No branches or pull requests

2 participants