From 2ca9996d7e80b5e1683c3bd7cc0632178bc2d97a Mon Sep 17 00:00:00 2001 From: Davide Segullo Date: Fri, 7 Jun 2024 10:41:18 -0500 Subject: [PATCH] feat: add price axis to charts (#3304) * feat: :sparkles: improve chart rendering * feat: :sparkles: add chart right price scale * feat: :lipstick: improve char tooltip format * feat: :sparkles: improve price axis formatting * feat: :sparkles: add horizontal line for crosshair * feat: :sparkles: remove crosshair labels from chart * feat: :sparkles: add hover date belove chart header price * feat: :sparkles: display hover date only on actual hover * fix: :bug: fix utc date formatting --- .../chart/light-weight-charts/chart.tsx | 69 +++++++++---------- .../chart/light-weight-charts/linear-chart.ts | 3 +- .../chart/light-weight-charts/utils.ts | 41 +++++++++++ .../components/chart/price-historical-v2.tsx | 24 ++++--- .../web/components/chart/price-historical.tsx | 64 ++++++++++------- .../hooks/ui-config/use-asset-info-config.ts | 26 ++++++- packages/web/pages/assets/[denom].tsx | 3 +- 7 files changed, 159 insertions(+), 71 deletions(-) create mode 100644 packages/web/components/chart/light-weight-charts/utils.ts diff --git a/packages/web/components/chart/light-weight-charts/chart.tsx b/packages/web/components/chart/light-weight-charts/chart.tsx index 76047d8d67..0d0ee8b596 100644 --- a/packages/web/components/chart/light-weight-charts/chart.tsx +++ b/packages/web/components/chart/light-weight-charts/chart.tsx @@ -1,7 +1,6 @@ import { ColorType, DeepPartial, - isBusinessDay, LineStyle, MouseEventParams, TickMarkType, @@ -17,6 +16,10 @@ import React, { useSyncExternalStore, } from "react"; +import { + priceFormatter, + timepointToString, +} from "~/components/chart/light-weight-charts/utils"; import { theme } from "~/tailwind.config"; import { @@ -33,38 +36,6 @@ function resizeSubscribe(callback: (this: Window, ev: UIEvent) => unknown) { }; } -const timepointToString = ( - timePoint: Time, - formatOptions: Intl.DateTimeFormatOptions, - locale?: string -) => { - let date = new Date(); - - if (typeof timePoint === "string") { - date = new Date(timePoint); - } else if (!isBusinessDay(timePoint)) { - date = new Date((timePoint as number) * 1000); - } else { - date = new Date( - Date.UTC(timePoint.year, timePoint.month - 1, timePoint.day) - ); - } - - // from given date we should use only as UTC date or timestamp - // but to format as locale date we can convert UTC date to local date - const localDateFromUtc = new Date( - date.getUTCFullYear(), - date.getUTCMonth(), - date.getUTCDate(), - date.getUTCHours(), - date.getUTCMinutes(), - date.getUTCSeconds(), - date.getUTCMilliseconds() - ); - - return localDateFromUtc.toLocaleString(locale, formatOptions); -}; - export const defaultOptions: DeepPartial = { layout: { fontFamily: theme.fontFamily.subtitle1.join(","), @@ -76,15 +47,38 @@ export const defaultOptions: DeepPartial = { fontSize: 14, }, grid: { horzLines: { visible: false }, vertLines: { visible: false } }, - rightPriceScale: { visible: false }, - leftPriceScale: { visible: false }, + rightPriceScale: { + autoScale: true, + borderVisible: false, + ticksVisible: false, + scaleMargins: { + top: 0.25, + bottom: 0, + }, + }, + leftPriceScale: { + autoScale: true, + borderVisible: false, + ticksVisible: false, + scaleMargins: { + top: 0.25, + bottom: 0, + }, + }, crosshair: { - horzLine: { visible: false }, + horzLine: { + labelBackgroundColor: theme.colors.osmoverse[850], + style: LineStyle.LargeDashed, + width: 2, + color: `${theme.colors.osmoverse[300]}33`, + labelVisible: false, + }, vertLine: { labelBackgroundColor: theme.colors.osmoverse[850], style: LineStyle.LargeDashed, width: 2, color: `${theme.colors.osmoverse[300]}33`, + labelVisible: false, }, }, handleScroll: false, @@ -94,6 +88,7 @@ export const defaultOptions: DeepPartial = { mouse: false, }, localization: { + priceFormatter, timeFormatter: (timePoint: Time) => { const formatOptions: Intl.DateTimeFormatOptions = { year: "numeric", @@ -206,7 +201,7 @@ export const Chart = memo( }, []); return ( -
+
{children}
); diff --git a/packages/web/components/chart/light-weight-charts/linear-chart.ts b/packages/web/components/chart/light-weight-charts/linear-chart.ts index f95f473467..c140365ae6 100644 --- a/packages/web/components/chart/light-weight-charts/linear-chart.ts +++ b/packages/web/components/chart/light-weight-charts/linear-chart.ts @@ -63,10 +63,11 @@ export class LinearChartController extends AreaChartController { return `
- $ ${ formatPretty(closeDec, { maxDecimals, + currency: "USD", + style: "currency", ...formatOpts, }) || "" } diff --git a/packages/web/components/chart/light-weight-charts/utils.ts b/packages/web/components/chart/light-weight-charts/utils.ts new file mode 100644 index 0000000000..590f4a70af --- /dev/null +++ b/packages/web/components/chart/light-weight-charts/utils.ts @@ -0,0 +1,41 @@ +import { Dec } from "@keplr-wallet/unit"; +import { isBusinessDay, Time } from "lightweight-charts"; + +import { formatPretty, getPriceExtendedFormatOptions } from "~/utils/formatter"; +import { getDecimalCount } from "~/utils/number"; + +export const priceFormatter = (price: number) => { + const minimumDecimals = 2; + const maxDecimals = Math.max(getDecimalCount(price), minimumDecimals); + + const priceDec = new Dec(price); + + const formatOpts = getPriceExtendedFormatOptions(priceDec); + + return formatPretty(priceDec, { + maxDecimals, + currency: "USD", + style: "currency", + ...formatOpts, + }); +}; + +export const timepointToString = ( + timePoint: Time, + formatOptions: Intl.DateTimeFormatOptions, + locale?: string +) => { + let date = new Date(); + + if (typeof timePoint === "string") { + date = new Date(timePoint); + } else if (!isBusinessDay(timePoint)) { + date = new Date((timePoint as number) * 1000); + } else { + date = new Date( + Date.UTC(timePoint.year, timePoint.month - 1, timePoint.day) + ); + } + + return date.toLocaleString(locale, formatOptions); +}; diff --git a/packages/web/components/chart/price-historical-v2.tsx b/packages/web/components/chart/price-historical-v2.tsx index 3531f413a1..0890cf3086 100644 --- a/packages/web/components/chart/price-historical-v2.tsx +++ b/packages/web/components/chart/price-historical-v2.tsx @@ -2,32 +2,40 @@ import { AreaData, AreaSeriesOptions, DeepPartial, + Time, UTCTimestamp, } from "lightweight-charts"; import React, { FunctionComponent, memo } from "react"; -import { LinearChartController } from "~/components/chart/light-weight-charts/linear-chart"; +import { AreaChartController } from "~/components/chart/light-weight-charts/area-chart"; +import { theme } from "~/tailwind.config"; import { Chart } from "./light-weight-charts/chart"; const seriesOpt: DeepPartial = { - lineColor: "#8C8AF9", - topColor: "rgba(60, 53, 109, 1)", - bottomColor: "rgba(32, 27, 67, 1)", + lineColor: theme.colors.wosmongton[300], + topColor: theme.colors.osmoverse[700], + bottomColor: theme.colors.osmoverse[850], priceLineVisible: false, - priceScaleId: "left", + lastValueVisible: false, + priceScaleId: "right", crosshairMarkerBorderWidth: 0, crosshairMarkerRadius: 8, + priceFormat: { + type: "price", + precision: 10, + minMove: 0.0000001, + }, }; export const HistoricalPriceChartV2: FunctionComponent<{ data: { close: number; time: number }[]; - onPointerHover?: (price: number) => void; + onPointerHover?: (price: number, time: Time) => void; onPointerOut?: () => void; }> = memo(({ data = [], onPointerHover, onPointerOut }) => { return ( 0) { const [data] = [...params.seriesData.values()] as AreaData[]; - onPointerHover?.(data.value); + onPointerHover?.(data.value, data.time); } else { onPointerOut?.(); } diff --git a/packages/web/components/chart/price-historical.tsx b/packages/web/components/chart/price-historical.tsx index 07de50a271..40b7c2439e 100644 --- a/packages/web/components/chart/price-historical.tsx +++ b/packages/web/components/chart/price-historical.tsx @@ -250,6 +250,7 @@ export const PriceChartHeader: FunctionComponent<{ historicalRange: PriceRange; setHistoricalRange: (pr: PriceRange) => void; hoverPrice: number; + hoverDate?: string | null; decimal: number; formatOpts?: FormatOptions; fiatSymbol?: string; @@ -272,6 +273,7 @@ export const PriceChartHeader: FunctionComponent<{ setHistoricalRange, baseDenom, quoteDenom, + hoverDate, hoverPrice, formatOpts, decimal, @@ -318,29 +320,45 @@ export const PriceChartHeader: FunctionComponent<{ classes?.pricesHeaderContainerClass )} > - -

- {fiatSymbol} - {compactZeros ? ( - <> - {significantDigits}. - {Boolean(zeros) && ( - <> - 0{zeros} - - )} - {decimalDigits} - - ) : ( - getFormattedPrice() - )} -

-
+
+ +

+ {fiatSymbol} + {compactZeros ? ( + <> + {significantDigits}. + {Boolean(zeros) && ( + <> + 0{zeros} + + )} + {decimalDigits} + + ) : ( + getFormattedPrice() + )} +

+
+ {hoverDate !== undefined ? ( +

+ {hoverDate} +

+ ) : ( + false + )} +
{baseDenom && quoteDenom ? (
{ + readonly setHoverPrice = (price: number, time?: Time) => { this._hoverPrice = price; + this._hoverDate = time; }; @action diff --git a/packages/web/pages/assets/[denom].tsx b/packages/web/pages/assets/[denom].tsx index afa29758e2..67c4165082 100644 --- a/packages/web/pages/assets/[denom].tsx +++ b/packages/web/pages/assets/[denom].tsx @@ -476,6 +476,7 @@ const TokenChartHeader = observer(() => { decimal={maxDecimals} showAllRange hoverPrice={hoverPrice} + hoverDate={assetInfoConfig.hoverDate} historicalRange={assetInfoConfig.historicalRange} setHistoricalRange={assetInfoConfig.setHistoricalRange} fiatSymbol={fiatSymbol} @@ -503,7 +504,7 @@ const TokenChart = observer(() => { data={assetInfoConfig.historicalChartData} onPointerHover={assetInfoConfig.setHoverPrice} onPointerOut={() => { - assetInfoConfig.setHoverPrice(0); + assetInfoConfig.setHoverPrice(0, undefined); }} />