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.
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;