0

i have a lightweight chart component which renders two charts that i have synced together so they look like one chart with 2 panes. The issue i am having is both of the charts have different data so the price scale width is inconsistent between the charts.

enter image description here

if you look at the price scale , you can see that the bottom pane seems to have less width because the price range is smaller. I want to make them look completely as one, any help would be appreciated.

The Chart Component:


import * as charts from "lightweight-charts";
import {
  ChartOptions,
  DeepPartial,
  IChartApi,
  UTCTimestamp,
} from "lightweight-charts";
import { useEffect, useRef, useState } from "react";
import { maxBy } from "lodash";
import { DailyPriceEntry } from "@tweevest/types";
import dayjs from "dayjs";
import utc from "dayjs/plugin/utc";
import timezone from "dayjs/plugin/timezone";
import advancedFormat from "dayjs/plugin/advancedFormat";
import { roundLargeNumbers, roundPrice } from "../../../../utils";
import { PriceChartEntry } from "../../../../types";

dayjs.extend(utc);
dayjs.extend(timezone);
dayjs.extend(advancedFormat);

const naText = "-";
const sma9Color = "black";
const sma50Color = "gray";
const sma200Color = "red";
const rsColor = "blue";
const upColor = "#2961FF";
const downColor = "#EC407A";

interface LegendData {
  open: number;
  high: number;
  low: number;
  close: number;
  volume: number | string;
  sma9: number;
  sma50: number;
  sma200: number;
  rs: number;
}

interface TradingViewProps {
  chartData: PriceChartEntry[];
  dailyPriceEntry: DailyPriceEntry[];
}

const chartOptions: DeepPartial<ChartOptions> = {
  // width: 00,
  grid: {
    horzLines: {
      visible: false,
    },
    vertLines: {
      visible: false,
    },
  },
  layout: {
    fontFamily: "GilmerMedium",
    fontSize: 12,
    textColor: "#727FA4",
  },
  timeScale: {
    timeVisible: true,
    borderVisible: false,
  },
  rightPriceScale: {
    autoScale: true,
    // borderColor: "red",
    entireTextOnly: false,
    visible: true,
    // drawTicks: true,
    alignLabels: true,
    // invertScale: true,

    scaleMargins: {
      top: 0,
      bottom: 0.15,
    },
  },
  leftPriceScale: {
    borderColor: "#727FA4",
    entireTextOnly: true,
    visible: false,
    drawTicks: true,
    scaleMargins: {
      top: 0.5,
      bottom: 0.1,
    },
  },
};

const calculatePerformance = (data: DailyPriceEntry[]) => {
  if (data && data.length > 0) {
    const response =
      ((data[data.length - 1].adjClose - data[0].adjClose) / data[0].adjClose) *
      100;
    return parseFloat(response.toFixed(2));
  }
  return 0.0;
};
const getRangeData = (data: DailyPriceEntry[], interval: number) => {
  if (data.length > 0) {
    const prevDate = dayjs().subtract(interval, "days");

    return data
      .filter((obj) => {
        const stockDate = dayjs(obj.date);
        return stockDate >= prevDate;
      })
      .reverse();
  }
  return [];
};

function movingAvg(array: never[], count: number, key?: string) {
  const result = [];
  // eslint-disable-next-line no-plusplus
  for (let i = 0; i < array.length; i++) {
    const x = array[i];
    const subArr = array.slice(
      Math.max(i - count, 0),
      Math.min(i + 1, array.length)
    );
    const avg =
      subArr.reduce((a, b) => {
        if (key) {
          return a + (Number.isNaN(b[key]) ? 0 : b[key]);
        }
        return a + (Number.isNaN(b) ? 0 : b);
      }, 0) / subArr.length;
    if (key) {
      // @ts-ignore
      result.push({ ...x, average: avg });
    } else {
      result.push(avg);
    }
  }
  // eslint-disable-next-line
  return result;
}

function TradingViewChart({ chartData, dailyPriceEntry }: TradingViewProps) {
  const chartRef = useRef<HTMLDivElement>(null);
  const chartRef2 = useRef<HTMLDivElement>(null);
  const chart = useRef<IChartApi>();
  const chart2 = useRef<IChartApi>();
  const [legendData, setLegendData] = useState<LegendData | null>(null);

  // sort the data, otherwise lightweight-charts throws an error
  const sorted = chartData.sort(
    (a, b) => new Date(a.date).valueOf() - new Date(b.date).valueOf()
  );

  // eslint-disable-next-line
  const maxValue = maxBy(sorted, (x: { high: any; }) => x.high)?.high || 0;

  const candlestickData = sorted.map((x) => ({
    ...x,
    time: (Date.parse(x.date) / 1000) as UTCTimestamp,
  }));
  const volumeData = sorted.map((x) => ({
    time: (Date.parse(x.date) / 1000) as UTCTimestamp,
    value: x.volume ?? 0,
    color: x.close - x.open > 0 ? upColor : downColor,
  }));

  const rsData = sorted.map((x) => ({
    time: (Date.parse(x.date) / 1000) as UTCTimestamp,
    value: x.rs ?? 0,
  }));

  const sma9 = movingAvg(sorted as never[], 9, "close").map((x) => ({
    time: (Date.parse(x.date) / 1000) as UTCTimestamp,
    value: x.average ?? 0,
  }));
  const sma50 = movingAvg(sorted as never[], 50, "close").map((x) => ({
    time: (Date.parse(x.date) / 1000) as UTCTimestamp,
    value: x.average ?? 0,
  }));
  const sma200 = movingAvg(sorted as never[], 200, "close").map((x) => ({
    time: (Date.parse(x.date) / 1000) as UTCTimestamp,
    value: x.average ?? 0,
  }));

  useEffect(() => {
    if (chartRef && chartRef2 && !chart.current && !chart2.current) {
      chart.current = charts.createChart("priceChart", {
        ...chartOptions,
        height: 300,
      });
      chart2.current = charts.createChart("rsChart", {
        ...chartOptions,
        height: 110,
      });
      chart.current!.applyOptions({
        width: chartRef.current!.offsetWidth,
      });
      chart2.current!.applyOptions({
        width: chartRef2.current!.offsetWidth,
      });
      // Bar chart
      const bar = chart.current.addBarSeries({
        thinBars: false,
        priceLineVisible: false,
        upColor,
        downColor,

        // title: "Price",
        priceFormat: {
          minMove: maxValue > 1 ? 1 : 0.01,
        },
      });
      bar.setData(candlestickData);

      // Volume chart
      const volume = chart.current.addHistogramSeries({
        priceFormat: {
          precision: 1,
          type: "volume",
        },
        priceLineVisible: false,
        lastValueVisible: false,
        priceScaleId: "volume",
        scaleMargins: {
          top: 0.85,
          bottom: 0,
        },
      });

      volume.setData(volumeData);
      // Moving average charts
      const line9 = chart.current.addLineSeries({
        color: sma9Color,
        // title: "ma9",
        priceLineVisible: false,
        crosshairMarkerVisible: false,
        lastValueVisible: false,
        lineWidth: 1,
      });
      line9.setData(sma9);
      const line50 = chart.current.addLineSeries({
        color: sma50Color,
        // title: "ma50",
        priceLineVisible: false,
        crosshairMarkerVisible: false,
        lastValueVisible: false,
        lineWidth: 1,
      });
      line50.setData(sma50);
      const line200 = chart.current.addLineSeries({
        color: sma200Color,
        // title: "ma200",
        priceLineVisible: false,
        crosshairMarkerVisible: false,
        lastValueVisible: false,
        lineWidth: 1,
      });
      line200.setData(sma200);

      // RS line
      const rsLine = chart2.current.addLineSeries({
        priceScaleId: "right",
        priceFormat: {
          // minMove: 1,
          type: "volume",
          precision: 2,
        },
        color: rsColor,
        priceLineVisible: false,
        crosshairMarkerVisible: false,
        title: "RS",
        lastValueVisible: true,
        lineWidth: 1,
      });
      rsLine.setData(rsData);
      let price;
      let vol;
      let s9;
      let s50;
      let s200;
      let rs;
      // Legend
      chart.current.subscribeCrosshairMove((param) => {
        // @ts-ignore
        price = param.seriesPrices.get(bar);
        // @ts-ignore
        vol = param.seriesPrices.get(volume);
        // @ts-ignore
        s9 = param.seriesPrices.get(line9);
        // @ts-ignore
        s50 = param.seriesPrices.get(line50);
        // @ts-ignore
        s200 = param.seriesPrices.get(line200);
        if (!price) return;

        const legendDataLocal: Omit<LegendData, "rs"> = {
          // @ts-ignore
          open: roundPrice(price.open),
          // @ts-ignore
          low: roundPrice(price.low),
          // @ts-ignore
          high: roundPrice(price.high),
          // @ts-ignore
          close: roundPrice(price.close),
          // @ts-ignore
          volume: `${roundLargeNumbers(vol).value}${
            // @ts-ignore
            roundLargeNumbers(vol).unit
          }`,
          // @ts-ignore
          sma9: roundPrice(s9),
          // @ts-ignore
          sma50: roundPrice(s50),
          // @ts-ignore
          sma200: roundPrice(s200),
        };
        setLegendData((prev) =>
          prev && prev.rs
            ? {
                ...legendDataLocal,
                rs: prev.rs,
              }
            : {
                ...legendDataLocal,
                rs: 0,
              }
        );
      });
      chart2.current.subscribeCrosshairMove((param) => {
        // @ts-ignore
        rs = param.seriesPrices.get(rsLine);
        if (!rs) return;

        const legendDataLocal: Pick<LegendData, "rs"> = {
          // @ts-ignore
          rs: Math.round(rs),
        };
        setLegendData((prev) =>
          prev
            ? {
                ...prev,
                rs: legendDataLocal.rs,
              }
            : ({
                rs: legendDataLocal.rs,
              } as LegendData)
        );
      });
      window.onresize = () => {
        chart.current!.applyOptions({
          width: chartRef.current!.offsetWidth,
          height: chartRef.current!.offsetHeight,
        });
        chart2.current!.applyOptions({
          width: chartRef2.current!.offsetWidth,
          height: chartRef2.current!.offsetHeight,
        });
      };
    } else {
      console.log("NO CHART");
    }
    }, []); //eslint-disable-line


  useEffect(() => {
    chart.current?.timeScale().subscribeVisibleLogicalRangeChange((range) => {
      chart2.current?.timeScale().setVisibleLogicalRange(range!);
    });
    chart2.current?.timeScale().subscribeVisibleLogicalRangeChange((range) => {
      chart.current?.timeScale().setVisibleLogicalRange(range!);
    });
  }, []);
  
  return (
    <div className="">
      <div
        id="legend"
        className="secondary heading-XXS"
        style={{
          marginLeft: "1rem",
          marginBottom: "0.5rem",
          fontFamily: "unset",
        }}
      >
        <div className="flex items-center gap-3 md:flex-wrap mb-2.5">
          <span className="rounded-lg py-[5px] px-2.5 bg-gray-50 heading-XS text-gray-500 border border-gray-200">
            Open: {legendData?.open || naText}
          </span>
          <span className="rounded-lg py-[5px] px-2.5 bg-gray-50 heading-XS text-gray-500 border border-gray-200">
            Low: {legendData?.low || naText}
          </span>
          <span className="rounded-lg py-[5px] px-2.5 bg-gray-50 heading-XS text-gray-500 border border-gray-200">
            High: {legendData?.high || naText}
          </span>
          <span className="rounded-lg py-[5px] px-2.5 bg-gray-50 heading-XS text-gray-500 border border-gray-200">
            Close: {legendData?.close || naText}
          </span>
          <span className="rounded-lg py-[5px] px-2.5 bg-gray-50 heading-XS text-gray-500 border border-gray-200">
            Vol: {legendData?.volume || naText}
          </span>
          <span className="rounded-lg py-[5px] px-2.5 bg-gray-200 heading-XS text-gray-900 border border-gray-200">
            MA9: {legendData?.sma9 || naText}
          </span>
          <span className="rounded-lg py-[5px] px-2.5 bg-gray-50 heading-XS text-gray-500 border border-gray-200">
            MA50: {legendData?.sma50 || naText}
          </span>
          <span className="rounded-lg py-[5px] px-2.5 bg-red-50 heading-XS text-red-500 border border-red-200">
            MA200: {legendData?.sma200 || naText}
          </span>
          <span className="rounded-lg py-[5px] px-2.5 bg-blue-200 heading-XS text-blue-500 border border-blue-400">
            RS: {legendData?.rs || naText}
          </span>
        </div>
      </div>
      <div>
        <div
          className="chart max-w-[97%] mx-auto"
          id="priceChart"
          ref={chartRef}
          // onMouseEnter={onMouseEnter}
          // onMouseLeave={onMouseLeave}
        />
        <div
          className="chart mt-[-25px] max-w-[97%] mx-auto"
          id="rsChart"
          ref={chartRef2}
          // onMouseEnter={onMouseEnter2}
          // onMouseLeave={onMouseLeave2}
        />
      </div>
    </div>
  );
}

export default TradingViewChart;

0 Answers0