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

charts enhancement #113

Closed
wants to merge 25 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
08853c6
chart styling 1st
maelsar Sep 2, 2023
74de128
TF selector buttons
maelsar Sep 2, 2023
53ebf23
Working Chart - before refactoring
maelsar Sep 6, 2023
f7939a6
update2 before refactor
maelsar Sep 6, 2023
aa41128
Merge branch 'DeXter-on-Radix:main' into redux-charts-enhancement
maelsar Sep 10, 2023
a068194
Synced with upstream/main
maelsar Sep 10, 2023
10a0fae
Refactored crosshair data (for legend) to use redux store
maelsar Sep 10, 2023
9c338f8
Moved helper functions to utils. Moved Crosshair data fetching to slice
maelsar Sep 10, 2023
ea0f07b
fixed eslint warning
maelsar Sep 10, 2023
38ea250
removed pairname from selector bar
maelsar Sep 10, 2023
d741fb1
price selector and cursor prices (legend) applied to charts component
maelsar Sep 11, 2023
ba0a17e
added color formatting to legend values and refactored "Change" logic
maelsar Sep 11, 2023
c8f87f0
Updated volumeSeries type to NOT "any" -- Updated Legend Data "Change…
maelsar Sep 12, 2023
2a19b52
refactored volume bars logic and updated color change
maelsar Sep 12, 2023
95de744
added async thunk to update legend values to latest candle values
maelsar Sep 12, 2023
68ead59
Removed unused variables and cleaned up comments
maelsar Sep 13, 2023
ddae4d8
Merge branch 'DeXter-on-Radix:main' into redux-charts-enhancement
maelsar Sep 13, 2023
8af4225
Merge remote-tracking branch 'upstream/main' into redux-charts-enhanc…
maelsar Sep 13, 2023
cfca325
Merge branch 'redux-charts-enhancement' of https://github.com/maelsar…
maelsar Sep 13, 2023
3469da7
Updated to correct after sync.
maelsar Sep 13, 2023
b141a31
Merge branch 'DeXter-on-Radix:main' into redux-charts-enhancement
maelsar Sep 13, 2023
b637a7b
Merge branch 'DeXter-on-Radix:main' into redux-charts-enhancement
maelsar Sep 15, 2023
2045cef
Removed async thunk
maelsar Sep 15, 2023
0b4077c
Removed the gap between the legend bar and chart
maelsar Sep 15, 2023
8a8160e
Merge branch 'DeXter-on-Radix:main' into redux-charts-enhancement
maelsar Sep 23, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
157 changes: 128 additions & 29 deletions src/app/components/PriceChart.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,30 +4,51 @@ import {
CANDLE_PERIODS,
OHLCVData,
setCandlePeriod,
handleCrosshairMove,
// fetchCandlesForInitialPeriod,
initializeLegend,
} from "../redux/priceChartSlice";
import { useAppDispatch, useAppSelector } from "../hooks";
import { formatPercentageChange } from "../utils";

interface PriceChartProps {
data: OHLCVData[];
}

function PriceChartCanvas(props: PriceChartProps) {
const chartContainerRef = useRef<HTMLDivElement>(null);
const dispatch = useAppDispatch();
const { data } = props;

useEffect(() => {
const chartContainer = chartContainerRef.current;

// dispatch(fetchCandlesForInitialPeriod());
if (data && data.length > 0) {
dispatch(initializeLegend());
}

if (chartContainer) {
const handleResize = () => {
chart.applyOptions({ width: chartContainer.clientWidth });
};

const chart = createChart(chartContainer, {
width: chartContainer.clientWidth,
height: 450,

// TODO: timescale is not visible
height: 500,
//MODIFY THEME COLOR HERE
layout: {
background: { color: "#181c27" },
textColor: "#DDD",
},
//MODIFY THEME COLOR HERE
grid: {
vertLines: { color: "#444" },
horzLines: { color: "#444" },
},
timeScale: {
//MODIFY THEME COLOR HERE
borderColor: "#d3d3d4",
Comment on lines +41 to +51
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These colors probably can also come from the theme settings. You can find what colors we currently have at tailwind.config.js under daisyui objects (only dark theme currently).

Here is the list of color names and variables that you can use - https://daisyui.com/docs/colors/#-2 . Here in the chart settings you can use them as variables (e.g. hsl(var(--b1))). In other places where you style an html element in .tsx you can specify them as classes, e.g. for background bg-base-100 or for text color text-base-100.

timeVisible: true,
},
});
Expand All @@ -37,30 +58,45 @@ function PriceChartCanvas(props: PriceChartProps) {
// OHLC
const ohlcSeries = chart.addCandlestickSeries({});
ohlcSeries.setData(clonedData);

chart.priceScale("right").applyOptions({
//MODIFY THEME COLOR HERE
borderColor: "#d3d3d4",
scaleMargins: {
top: 0,
bottom: 0.2,
top: 0.1,
bottom: 0.3,
},
});

// Volume
// Volume Initialization
const volumeSeries = chart.addHistogramSeries({
priceFormat: {
type: "volume",
},
priceScaleId: "volume",
color: "#eaeff5",
});

volumeSeries.setData(clonedData);
// VOLUME BARS
// MODIFY THEME COLOR HERE
volumeSeries.setData(
data.map((datum) => ({
...datum,
color: datum.close - datum.open <= 0 ? "#ef5350" : "#26a69a",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These colors should come from css variables. As strings in code they can be represented as hsl(var(--su)) for green, hsl(var(--er)) for red.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When I tried to use the hsl(var(--su)) variables it cause the chart to be black (randomly hit or miss), I suspect it was because the library does some extra steps with the string
image

Copy link
Member

@EvgeniiaVak EvgeniiaVak Oct 3, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, probably we can import tailwind config object then and take the color from there (I guess only if it goes inside the canvas), we'll need to handle dark/light switching ourselves (but that's after-mvp).

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I did find this fiddle that will help with that after the mvp.
https://jsfiddle.net/TradingView/6yLzrbtd/

}))
);

// volumeSeries.setData(clonedData);
chart.priceScale("volume").applyOptions({
scaleMargins: {
top: 0.8,
bottom: 0,
bottom: 0.01,
},
});

//Crosshair Data for legend
dispatch(handleCrosshairMove(chart, data, volumeSeries));

//Prevent Chart from clipping
const chartDiv = chartContainer.querySelector(".tv-lightweight-charts");
if (chartDiv) {
(chartDiv as HTMLElement).style.overflow = "visible";
Expand All @@ -70,37 +106,100 @@ function PriceChartCanvas(props: PriceChartProps) {

return () => {
window.removeEventListener("resize", handleResize);
// clearInterval(intervalId);
chart.remove();
};
}
}, [data]);

return <div ref={chartContainerRef} />;
}, [data, dispatch]);
//Temporary brute force approach to trim the top of the chart to remove the gap
return <div ref={chartContainerRef} className="relative mt-[-1.7rem]"></div>;
}

export function PriceChart() {
const state = useAppSelector((state) => state.priceChart);
const dispatch = useAppDispatch();
const candlePeriod = useAppSelector((state) => state.priceChart.candlePeriod);
const candlePrice = useAppSelector(
(state) => state.priceChart.legendCandlePrice
);
const change = useAppSelector((state) => state.priceChart.legendChange);
const percChange = useAppSelector(
(state) => state.priceChart.legendPercChange
);
const currentVolume = useAppSelector(
(state) => state.priceChart.legendCurrentVolume
);
const isNegativeOrZero = useAppSelector(
(state) => state.priceChart.isNegativeOrZero
);

return (
<div>
<label htmlFor="candle-period-selector">Candle Period:</label>
<select
className="select select-ghost"
id="candle-period-selector"
value={state.candlePeriod}
onChange={(e) => {
dispatch(setCandlePeriod(e.target.value));
}}
>
{CANDLE_PERIODS.map((period) => (
<option key={period} value={period}>
{period}
</option>
))}
</select>

<div className="">
<div className="flex p-[1vh]">
{CANDLE_PERIODS.map((period) => (
<button
key={period}
className={`px-[0.5vw] py-[0.5vw] text-sm font-roboto text-#d4e7df hover:bg-white hover:bg-opacity-30 hover:rounded-md ${
candlePeriod === period ? "text-blue-500" : ""
}`}
onClick={() => dispatch(setCandlePeriod(period))}
>
{period}
</button>
))}
</div>
<div className="flex justify-between text-sm font-roboto">
<div className="ml-4">
Open:{" "}
<span
className={isNegativeOrZero ? "text-red-500" : "text-green-500"}
>
{candlePrice?.open}
</span>
</div>
<div>
High:{" "}
<span
className={isNegativeOrZero ? "text-red-500" : "text-green-500"}
Comment on lines +142 to +163
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here and below (and you probably can specify {isNegativeOrZero ? ...} one time in the enclosing div (the one with the <div className="flex justify-between) also please use the theme colors to make page look consistent.

Please also remove font-roboto I think it should be set up for the whole app.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I removed the font-roboto and swapped colors where I could in #145

>
{candlePrice?.high}
</span>
</div>
<div>
Low:{" "}
<span
className={isNegativeOrZero ? "text-red-500" : "text-green-500"}
>
{candlePrice?.low}
</span>
</div>
<div>
Close:{" "}
<span
className={isNegativeOrZero ? "text-red-500" : "text-green-500"}
>
{candlePrice?.close}
</span>
</div>
<div>
Volume:{" "}
<span
className={isNegativeOrZero ? "text-red-500" : "text-green-500"}
>
{currentVolume === 0 ? 0 : currentVolume.toFixed(2)}
</span>
</div>
<div className="mr-4">
Change:{" "}
<span
className={isNegativeOrZero ? "text-red-500" : "text-green-500"}
>
{change}
{formatPercentageChange(percChange)}
</span>
</div>
</div>
</div>
<PriceChartCanvas data={state.ohlcv} />
</div>
);
Expand Down
116 changes: 113 additions & 3 deletions src/app/redux/priceChartSlice.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,14 @@
import * as adex from "alphadex-sdk-js";
import { PayloadAction, createSlice } from "@reduxjs/toolkit";
import { CandlestickData, UTCTimestamp } from "lightweight-charts";
import {
CandlestickData,
IChartApi,
UTCTimestamp,
ISeriesApi,
SeriesOptionsMap,
} from "lightweight-charts";
import { AppDispatch } from "./store";

export interface OHLCVData extends CandlestickData {
value: number;
}
Expand All @@ -10,11 +18,21 @@ export const CANDLE_PERIODS = adex.CandlePeriods;
export interface PriceChartState {
candlePeriod: string;
ohlcv: OHLCVData[];
legendCandlePrice: OHLCVData | null;
legendChange: number | null;
legendPercChange: number | null;
legendCurrentVolume: number;
isNegativeOrZero: boolean;
}

const initialState: PriceChartState = {
candlePeriod: adex.CandlePeriods[0],
candlePeriod: adex.CandlePeriods[2],
ohlcv: [],
legendCandlePrice: null,
legendPercChange: null,
legendChange: null,
legendCurrentVolume: 0,
isNegativeOrZero: false,
};

function cleanData(data: OHLCVData[]): OHLCVData[] {
Expand All @@ -39,6 +57,37 @@ function cleanData(data: OHLCVData[]): OHLCVData[] {
return cleanedData;
}

//Chart Crosshair
export function handleCrosshairMove(
chart: IChartApi,
data: OHLCVData[],
volumeSeries: ISeriesApi<keyof SeriesOptionsMap>
) {
return (dispatch: AppDispatch) => {
chart.subscribeCrosshairMove((param) => {
if (param.time) {
const currentIndex = data.findIndex(
(candle) => candle.time === param.time
);

if (currentIndex > 0 && currentIndex < data.length) {
const currentData = data[currentIndex];
const volumeData = param.seriesData.get(volumeSeries) as OHLCVData;
dispatch(setLegendChange(currentData));
dispatch(setLegendCandlePrice(currentData));
dispatch(
setLegendPercChange({
currentOpen: currentData.open,
currentClose: currentData.close,
})
);
dispatch(setLegendCurrentVolume(volumeData ? volumeData.value : 0));
}
}
});
};
}

function convertAlphaDEXData(data: adex.Candle[]): OHLCVData[] {
let tradingViewData = data.map((row): OHLCVData => {
const time = (new Date(row.startTime).getTime() / 1000) as UTCTimestamp;
Expand All @@ -65,7 +114,68 @@ export const priceChartSlice = createSlice({
updateCandles: (state, action: PayloadAction<adex.Candle[]>) => {
state.ohlcv = convertAlphaDEXData(action.payload);
},
setLegendCandlePrice: (state, action: PayloadAction<OHLCVData | null>) => {
state.legendCandlePrice = action.payload;
if (action.payload) {
state.isNegativeOrZero =
action.payload.close - action.payload.open <= 0;
}
},
setLegendChange: (state, action: PayloadAction<OHLCVData>) => {
if (action.payload) {
const difference = action.payload.close - action.payload.open;
state.legendChange = difference;
} else {
state.legendChange = null;
}
},
setLegendPercChange: (
state,
action: PayloadAction<{ currentOpen: number; currentClose: number }>
) => {
const { currentOpen, currentClose } = action.payload;
if (currentOpen !== null && currentClose !== null) {
const difference = currentClose - currentOpen;
let percentageChange = (difference / currentOpen) * 100;

if (Math.abs(percentageChange) < 0.01) {
percentageChange = 0;
}

state.legendPercChange = parseFloat(percentageChange.toFixed(2));
} else {
state.legendPercChange = null;
}
},
initializeLegend: (state) => {
if (state.ohlcv && state.ohlcv.length > 0) {
const latestOHLCVData = state.ohlcv[state.ohlcv.length - 1];
state.legendCandlePrice = latestOHLCVData;
state.legendChange = latestOHLCVData.close - latestOHLCVData.open;
state.legendPercChange = parseFloat(
(
((latestOHLCVData.close - latestOHLCVData.open) /
latestOHLCVData.open) *
100
).toFixed(2)
);
state.legendCurrentVolume = latestOHLCVData.value;
state.isNegativeOrZero =
latestOHLCVData.close - latestOHLCVData.open <= 0;
}
},
setLegendCurrentVolume: (state, action: PayloadAction<number>) => {
state.legendCurrentVolume = action.payload;
},
},
});

export const { setCandlePeriod, updateCandles } = priceChartSlice.actions;
export const {
setCandlePeriod,
updateCandles,
setLegendCandlePrice,
setLegendChange,
setLegendPercChange,
setLegendCurrentVolume,
initializeLegend,
} = priceChartSlice.actions;
8 changes: 8 additions & 0 deletions src/app/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -293,3 +293,11 @@ export function calculateTotalFees(order: any): number {
? roundTo(totalFees, 4, RoundType.NEAREST)
: totalFees;
}

//Chart Helper Functions
export const formatPercentageChange = (percChange: number | null): string => {
if (percChange !== null) {
return `(${percChange.toFixed(2)}%)`;
}
return "(0.00%)";
};