diff --git a/deploy/values/review/values.yaml.gotmpl b/deploy/values/review/values.yaml.gotmpl index 9242798443..b720f05d2b 100644 --- a/deploy/values/review/values.yaml.gotmpl +++ b/deploy/values/review/values.yaml.gotmpl @@ -51,8 +51,8 @@ frontend: NEXT_PUBLIC_FEATURED_NETWORKS: https://raw.githubusercontent.com/blockscout/frontend-configs/dev/configs/featured-networks/eth-goerli.json NEXT_PUBLIC_NETWORK_LOGO: https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/network-logos/goerli.svg NEXT_PUBLIC_NETWORK_ICON: https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/network-icons/goerli.svg - NEXT_PUBLIC_API_HOST: blockscout-main.k8s-dev.blockscout.com - NEXT_PUBLIC_STATS_API_HOST: https://stats-test.k8s-dev.blockscout.com/ + NEXT_PUBLIC_API_HOST: etc.blockscout.com + NEXT_PUBLIC_STATS_API_HOST: https://stats-etc.k8s.blockscout.com/ NEXT_PUBLIC_VISUALIZE_API_HOST: http://visualizer-svc.visualizer-testing.svc.cluster.local/ NEXT_PUBLIC_CONTRACT_INFO_API_HOST: https://contracts-info-test.k8s-dev.blockscout.com NEXT_PUBLIC_ADMIN_SERVICE_API_HOST: https://admin-rs-test.k8s-dev.blockscout.com diff --git a/ui/address/__screenshots__/AddressCoinBalance.pw.tsx_dark-color-mode_base-view-dark-mode-mobile-1.png b/ui/address/__screenshots__/AddressCoinBalance.pw.tsx_dark-color-mode_base-view-dark-mode-mobile-1.png index 45a0d12d91..39d86ce69b 100644 Binary files a/ui/address/__screenshots__/AddressCoinBalance.pw.tsx_dark-color-mode_base-view-dark-mode-mobile-1.png and b/ui/address/__screenshots__/AddressCoinBalance.pw.tsx_dark-color-mode_base-view-dark-mode-mobile-1.png differ diff --git a/ui/address/__screenshots__/AddressCoinBalance.pw.tsx_default_base-view-dark-mode-mobile-1.png b/ui/address/__screenshots__/AddressCoinBalance.pw.tsx_default_base-view-dark-mode-mobile-1.png index 5d93702fbb..2e8c7bf712 100644 Binary files a/ui/address/__screenshots__/AddressCoinBalance.pw.tsx_default_base-view-dark-mode-mobile-1.png and b/ui/address/__screenshots__/AddressCoinBalance.pw.tsx_default_base-view-dark-mode-mobile-1.png differ diff --git a/ui/address/__screenshots__/AddressCoinBalance.pw.tsx_mobile_base-view-dark-mode-mobile-1.png b/ui/address/__screenshots__/AddressCoinBalance.pw.tsx_mobile_base-view-dark-mode-mobile-1.png index f6e5ba4aad..931f7e7b41 100644 Binary files a/ui/address/__screenshots__/AddressCoinBalance.pw.tsx_mobile_base-view-dark-mode-mobile-1.png and b/ui/address/__screenshots__/AddressCoinBalance.pw.tsx_mobile_base-view-dark-mode-mobile-1.png differ diff --git a/ui/home/indicators/ChainIndicatorChart.tsx b/ui/home/indicators/ChainIndicatorChart.tsx index 023c79a892..46f3cd64a6 100644 --- a/ui/home/indicators/ChainIndicatorChart.tsx +++ b/ui/home/indicators/ChainIndicatorChart.tsx @@ -3,13 +3,11 @@ import React from 'react'; import type { TimeChartData } from 'ui/shared/chart/types'; -import useClientRect from 'lib/hooks/useClientRect'; import ChartArea from 'ui/shared/chart/ChartArea'; import ChartLine from 'ui/shared/chart/ChartLine'; import ChartOverlay from 'ui/shared/chart/ChartOverlay'; import ChartTooltip from 'ui/shared/chart/ChartTooltip'; import useTimeChartController from 'ui/shared/chart/useTimeChartController'; -import calculateInnerSize from 'ui/shared/chart/utils/calculateInnerSize'; interface Props { data: TimeChartData; @@ -22,12 +20,17 @@ const ChainIndicatorChart = ({ data }: Props) => { const overlayRef = React.useRef(null); const lineColor = useToken('colors', 'blue.500'); - const [ rect, ref ] = useClientRect(); - const { innerWidth, innerHeight } = calculateInnerSize(rect, CHART_MARGIN); - const { xScale, yScale } = useTimeChartController({ + const axesConfig = React.useMemo(() => { + return { + x: { ticks: 4 }, + y: { ticks: 3, nice: true }, + }; + }, [ ]); + + const { rect, ref, axis, innerWidth, innerHeight } = useTimeChartController({ data, - width: innerWidth, - height: innerHeight, + margin: CHART_MARGIN, + axesConfig, }); return ( @@ -35,13 +38,13 @@ const ChainIndicatorChart = ({ data }: Props) => { { anchorEl={ overlayRef.current } width={ innerWidth } height={ innerHeight } - xScale={ xScale } - yScale={ yScale } + xScale={ axis.x.scale } + yScale={ axis.y.scale } data={ data } /> diff --git a/ui/home/indicators/__screenshots__/ChainIndicators.pw.tsx_default_daily-txs-chart-dark-mode-mobile-1.png b/ui/home/indicators/__screenshots__/ChainIndicators.pw.tsx_default_daily-txs-chart-dark-mode-mobile-1.png index ffbad3afed..b354bd5198 100644 Binary files a/ui/home/indicators/__screenshots__/ChainIndicators.pw.tsx_default_daily-txs-chart-dark-mode-mobile-1.png and b/ui/home/indicators/__screenshots__/ChainIndicators.pw.tsx_default_daily-txs-chart-dark-mode-mobile-1.png differ diff --git a/ui/home/indicators/__screenshots__/ChainIndicators.pw.tsx_default_daily-txs-chart-mobile-1.png b/ui/home/indicators/__screenshots__/ChainIndicators.pw.tsx_default_daily-txs-chart-mobile-1.png index 157561a378..c2b269b416 100644 Binary files a/ui/home/indicators/__screenshots__/ChainIndicators.pw.tsx_default_daily-txs-chart-mobile-1.png and b/ui/home/indicators/__screenshots__/ChainIndicators.pw.tsx_default_daily-txs-chart-mobile-1.png differ diff --git a/ui/home/indicators/__screenshots__/ChainIndicators.pw.tsx_mobile_daily-txs-chart-dark-mode-mobile-1.png b/ui/home/indicators/__screenshots__/ChainIndicators.pw.tsx_mobile_daily-txs-chart-dark-mode-mobile-1.png index 978b265892..9a1b5d6269 100644 Binary files a/ui/home/indicators/__screenshots__/ChainIndicators.pw.tsx_mobile_daily-txs-chart-dark-mode-mobile-1.png and b/ui/home/indicators/__screenshots__/ChainIndicators.pw.tsx_mobile_daily-txs-chart-dark-mode-mobile-1.png differ diff --git a/ui/home/indicators/__screenshots__/ChainIndicators.pw.tsx_mobile_daily-txs-chart-mobile-1.png b/ui/home/indicators/__screenshots__/ChainIndicators.pw.tsx_mobile_daily-txs-chart-mobile-1.png index c8a2b9ab6a..8c76d6185c 100644 Binary files a/ui/home/indicators/__screenshots__/ChainIndicators.pw.tsx_mobile_daily-txs-chart-mobile-1.png and b/ui/home/indicators/__screenshots__/ChainIndicators.pw.tsx_mobile_daily-txs-chart-mobile-1.png differ diff --git a/ui/pages/__screenshots__/Home.pw.tsx_dark-color-mode_default-view---default-dark-mode-1.png b/ui/pages/__screenshots__/Home.pw.tsx_dark-color-mode_default-view---default-dark-mode-1.png index 203c44f09e..38b843009f 100644 Binary files a/ui/pages/__screenshots__/Home.pw.tsx_dark-color-mode_default-view---default-dark-mode-1.png and b/ui/pages/__screenshots__/Home.pw.tsx_dark-color-mode_default-view---default-dark-mode-1.png differ diff --git a/ui/pages/__screenshots__/Home.pw.tsx_default_default-view-screen-xl-1.png b/ui/pages/__screenshots__/Home.pw.tsx_default_default-view-screen-xl-1.png index c4086c53fc..e762dae528 100644 Binary files a/ui/pages/__screenshots__/Home.pw.tsx_default_default-view-screen-xl-1.png and b/ui/pages/__screenshots__/Home.pw.tsx_default_default-view-screen-xl-1.png differ diff --git a/ui/pages/__screenshots__/Home.pw.tsx_default_mobile-base-view-1.png b/ui/pages/__screenshots__/Home.pw.tsx_default_mobile-base-view-1.png index e3ddef99b4..20df208c57 100644 Binary files a/ui/pages/__screenshots__/Home.pw.tsx_default_mobile-base-view-1.png and b/ui/pages/__screenshots__/Home.pw.tsx_default_mobile-base-view-1.png differ diff --git a/ui/shared/chart/ChartWidgetGraph.tsx b/ui/shared/chart/ChartWidgetGraph.tsx index c2f5c2d433..92e1a04f26 100644 --- a/ui/shared/chart/ChartWidgetGraph.tsx +++ b/ui/shared/chart/ChartWidgetGraph.tsx @@ -1,11 +1,10 @@ import { useToken } from '@chakra-ui/react'; import * as d3 from 'd3'; -import React, { useEffect, useMemo } from 'react'; +import React from 'react'; import type { ChartMargin, TimeChartItem } from 'ui/shared/chart/types'; import dayjs from 'lib/date/dayjs'; -import useClientRect from 'lib/hooks/useClientRect'; import useIsMobile from 'lib/hooks/useIsMobile'; import ChartArea from 'ui/shared/chart/ChartArea'; import ChartAxis from 'ui/shared/chart/ChartAxis'; @@ -15,7 +14,6 @@ import ChartOverlay from 'ui/shared/chart/ChartOverlay'; import ChartSelectionX from 'ui/shared/chart/ChartSelectionX'; import ChartTooltip from 'ui/shared/chart/ChartTooltip'; import useTimeChartController from 'ui/shared/chart/useTimeChartController'; -import calculateInnerSize from 'ui/shared/chart/utils/calculateInnerSize'; interface Props { isEnlarged?: boolean; @@ -31,36 +29,54 @@ interface Props { const MAX_SHOW_ITEMS = 100_000_000_000; const DEFAULT_CHART_MARGIN = { bottom: 20, left: 40, right: 20, top: 10 }; -const ChartWidgetGraph = ({ isEnlarged, items, onZoom, isZoomResetInitial, title, margin, units }: Props) => { +const ChartWidgetGraph = ({ isEnlarged, items, onZoom, isZoomResetInitial, title, margin: marginProps, units }: Props) => { const isMobile = useIsMobile(); const color = useToken('colors', 'blue.200'); - const overlayRef = React.useRef(null); + const chartId = `chart-${ title.split(' ').join('') }-${ isEnlarged ? 'fullscreen' : 'small' }`; - const [ rect, ref ] = useClientRect(); - const chartMargin = { ...DEFAULT_CHART_MARGIN, ...margin }; - const { innerWidth, innerHeight } = calculateInnerSize(rect, chartMargin); + const overlayRef = React.useRef(null); - const chartId = `chart-${ title.split(' ').join('') }-${ isEnlarged ? 'fullscreen' : 'small' }`; const [ range, setRange ] = React.useState<[ Date, Date ]>([ items[0].date, items[items.length - 1].date ]); - const rangedItems = useMemo(() => + const rangedItems = React.useMemo(() => items.filter((item) => item.date >= range[0] && item.date <= range[1]), [ items, range ]); const isGroupedValues = rangedItems.length > MAX_SHOW_ITEMS; - const displayedData = useMemo(() => { + const displayedData = React.useMemo(() => { if (isGroupedValues) { return groupChartItemsByWeekNumber(rangedItems); } else { return rangedItems; } }, [ isGroupedValues, rangedItems ]); + const chartData = React.useMemo(() => ([ { items: displayedData, name: 'Value', color, units } ]), [ color, displayedData, units ]); - const { xTickFormat, yTickFormat, xScale, yScale } = useTimeChartController({ - data: [ { items: displayedData, name: title, color } ], - width: innerWidth, - height: innerHeight, + const margin: ChartMargin = React.useMemo(() => ({ ...DEFAULT_CHART_MARGIN, ...marginProps }), [ marginProps ]); + const axesConfig = React.useMemo(() => { + return { + x: { + ticks: isEnlarged ? 8 : 4, + }, + y: { + ticks: isEnlarged ? 6 : 3, + nice: true, + }, + }; + }, [ isEnlarged ]); + + const { + ref, + rect, + innerWidth, + innerHeight, + chartMargin, + axis, + } = useTimeChartController({ + data: chartData, + margin, + axesConfig, }); const handleRangeSelect = React.useCallback((nextRange: [ Date, Date ]) => { @@ -68,7 +84,7 @@ const ChartWidgetGraph = ({ isEnlarged, items, onZoom, isZoomResetInitial, title onZoom(); }, [ onZoom ]); - useEffect(() => { + React.useEffect(() => { if (isZoomResetInitial) { setRange([ items[0].date, items[items.length - 1].date ]); } @@ -80,8 +96,8 @@ const ChartWidgetGraph = ({ isEnlarged, items, onZoom, isZoomResetInitial, title @@ -90,14 +106,14 @@ const ChartWidgetGraph = ({ isEnlarged, items, onZoom, isZoomResetInitial, title id={ chartId } data={ displayedData } color={ color } - xScale={ xScale } - yScale={ yScale } + xScale={ axis.x.scale } + yScale={ axis.y.scale } /> @@ -127,15 +143,15 @@ const ChartWidgetGraph = ({ isEnlarged, items, onZoom, isZoomResetInitial, title width={ innerWidth } tooltipWidth={ isGroupedValues ? 280 : 200 } height={ innerHeight } - xScale={ xScale } - yScale={ yScale } + xScale={ axis.x.scale } + yScale={ axis.y.scale } data={ chartData } /> diff --git a/ui/shared/chart/__screenshots__/ChartWidget.pw.tsx_dark-color-mode_base-view-dark-mode-1.png b/ui/shared/chart/__screenshots__/ChartWidget.pw.tsx_dark-color-mode_base-view-dark-mode-1.png index 57dc0e647b..72eb0eca07 100644 Binary files a/ui/shared/chart/__screenshots__/ChartWidget.pw.tsx_dark-color-mode_base-view-dark-mode-1.png and b/ui/shared/chart/__screenshots__/ChartWidget.pw.tsx_dark-color-mode_base-view-dark-mode-1.png differ diff --git a/ui/shared/chart/__screenshots__/ChartWidget.pw.tsx_dark-color-mode_base-view-dark-mode-2.png b/ui/shared/chart/__screenshots__/ChartWidget.pw.tsx_dark-color-mode_base-view-dark-mode-2.png index 28f4c80bf8..e57274b035 100644 Binary files a/ui/shared/chart/__screenshots__/ChartWidget.pw.tsx_dark-color-mode_base-view-dark-mode-2.png and b/ui/shared/chart/__screenshots__/ChartWidget.pw.tsx_dark-color-mode_base-view-dark-mode-2.png differ diff --git a/ui/shared/chart/__screenshots__/ChartWidget.pw.tsx_dark-color-mode_base-view-dark-mode-3.png b/ui/shared/chart/__screenshots__/ChartWidget.pw.tsx_dark-color-mode_base-view-dark-mode-3.png index abcc2ed7c1..00e777d37d 100644 Binary files a/ui/shared/chart/__screenshots__/ChartWidget.pw.tsx_dark-color-mode_base-view-dark-mode-3.png and b/ui/shared/chart/__screenshots__/ChartWidget.pw.tsx_dark-color-mode_base-view-dark-mode-3.png differ diff --git a/ui/shared/chart/__screenshots__/ChartWidget.pw.tsx_dark-color-mode_base-view-dark-mode-4.png b/ui/shared/chart/__screenshots__/ChartWidget.pw.tsx_dark-color-mode_base-view-dark-mode-4.png index b313ab5464..3ce1a88ad4 100644 Binary files a/ui/shared/chart/__screenshots__/ChartWidget.pw.tsx_dark-color-mode_base-view-dark-mode-4.png and b/ui/shared/chart/__screenshots__/ChartWidget.pw.tsx_dark-color-mode_base-view-dark-mode-4.png differ diff --git a/ui/shared/chart/__screenshots__/ChartWidget.pw.tsx_default_base-view-dark-mode-1.png b/ui/shared/chart/__screenshots__/ChartWidget.pw.tsx_default_base-view-dark-mode-1.png index 2698e7d94d..808d2a135f 100644 Binary files a/ui/shared/chart/__screenshots__/ChartWidget.pw.tsx_default_base-view-dark-mode-1.png and b/ui/shared/chart/__screenshots__/ChartWidget.pw.tsx_default_base-view-dark-mode-1.png differ diff --git a/ui/shared/chart/__screenshots__/ChartWidget.pw.tsx_default_base-view-dark-mode-2.png b/ui/shared/chart/__screenshots__/ChartWidget.pw.tsx_default_base-view-dark-mode-2.png index 3d5211ec1d..79846d3704 100644 Binary files a/ui/shared/chart/__screenshots__/ChartWidget.pw.tsx_default_base-view-dark-mode-2.png and b/ui/shared/chart/__screenshots__/ChartWidget.pw.tsx_default_base-view-dark-mode-2.png differ diff --git a/ui/shared/chart/__screenshots__/ChartWidget.pw.tsx_default_base-view-dark-mode-3.png b/ui/shared/chart/__screenshots__/ChartWidget.pw.tsx_default_base-view-dark-mode-3.png index 1d4902ebde..513d3ffa65 100644 Binary files a/ui/shared/chart/__screenshots__/ChartWidget.pw.tsx_default_base-view-dark-mode-3.png and b/ui/shared/chart/__screenshots__/ChartWidget.pw.tsx_default_base-view-dark-mode-3.png differ diff --git a/ui/shared/chart/__screenshots__/ChartWidget.pw.tsx_default_base-view-dark-mode-4.png b/ui/shared/chart/__screenshots__/ChartWidget.pw.tsx_default_base-view-dark-mode-4.png index f8e85a8c61..2efba0462f 100644 Binary files a/ui/shared/chart/__screenshots__/ChartWidget.pw.tsx_default_base-view-dark-mode-4.png and b/ui/shared/chart/__screenshots__/ChartWidget.pw.tsx_default_base-view-dark-mode-4.png differ diff --git a/ui/shared/chart/types.tsx b/ui/shared/chart/types.tsx index ebc8fbc33d..80a023aa51 100644 --- a/ui/shared/chart/types.tsx +++ b/ui/shared/chart/types.tsx @@ -25,3 +25,13 @@ export interface TimeChartDataItem { } export type TimeChartData = Array; + +export interface AxisConfig { + ticks?: number; + nice?: boolean; +} + +export interface AxesConfig { + x?: AxisConfig; + y?: AxisConfig; +} diff --git a/ui/shared/chart/useTimeChartController.tsx b/ui/shared/chart/useTimeChartController.tsx index 1da6465413..a180f96831 100644 --- a/ui/shared/chart/useTimeChartController.tsx +++ b/ui/shared/chart/useTimeChartController.tsx @@ -1,101 +1,65 @@ -import * as d3 from 'd3'; -import { useMemo } from 'react'; +import React from 'react'; -import type { TimeChartData } from 'ui/shared/chart/types'; +import type { AxesConfig, ChartMargin, TimeChartData } from 'ui/shared/chart/types'; -import { WEEK, MONTH, YEAR } from 'lib/consts'; +import useClientRect from 'lib/hooks/useClientRect'; + +import calculateInnerSize from './utils/calculateInnerSize'; +import { getAxisParams, DEFAULT_MAXIMUM_SIGNIFICANT_DIGITS } from './utils/timeChartAxis'; interface Props { data: TimeChartData; - width: number; - height: number; + margin?: ChartMargin; + axesConfig?: AxesConfig; } -export default function useTimeChartController({ data, width, height }: Props) { - - const xMin = useMemo( - () => d3.min(data, ({ items }) => d3.min(items, ({ date }) => date)) || new Date(), - [ data ], - ); - - const xMax = useMemo( - () => d3.max(data, ({ items }) => d3.max(items, ({ date }) => date)) || new Date(), - [ data ], - ); - - const xScale = useMemo( - () => d3.scaleTime().domain([ xMin, xMax ]).range([ 0, width ]), - [ xMin, xMax, width ], - ); - - const yMin = useMemo( - () => d3.min(data, ({ items }) => d3.min(items, ({ value }) => value)) || 0, - [ data ], - ); - - const yMax = useMemo( - () => d3.max(data, ({ items }) => d3.max(items, ({ value }) => value)) || 0, - [ data ], - ); - - const yScale = useMemo(() => { - const indention = (yMax - yMin) * 0.15; - - return d3.scaleLinear() - .domain([ yMin >= 0 && yMin - indention <= 0 ? 0 : yMin - indention, yMax + indention ]) - .range([ height, 0 ]); - }, [ height, yMin, yMax ]); - - const yScaleForAxis = useMemo( - () => d3.scaleBand().domain([ String(yMin), String(yMax) ]).range([ height, 0 ]), - [ height, yMin, yMax ], - ); - - const xTickFormat = (axis: d3.Axis) => (d: d3.AxisDomain) => { - let format: (date: Date) => string; - const scale = axis.scale(); - const extent = scale.domain(); - - const span = Number(extent[1]) - Number(extent[0]); - - if (span > YEAR) { - format = d3.timeFormat('%Y'); - } else if (span > 2 * MONTH) { - format = d3.timeFormat('%b'); - } else if (span > WEEK) { - format = d3.timeFormat('%b %d'); - } else { - format = d3.timeFormat('%a %d'); - } - - return format(d as Date); - }; - - const yTickFormat = () => (d: d3.AxisDomain) => { - const num = Number(d); - const maximumFractionDigits = (() => { - if (num < 1) { - return 3; - } - - if (num < 10) { - return 2; - } - - if (num < 100) { - return 1; - } - - return 0; - })(); - return Number(d).toLocaleString(undefined, { maximumFractionDigits, notation: 'compact' }); - }; - - return { - xTickFormat, - yTickFormat, - xScale, - yScale, - yScaleForAxis, - }; +export default function useTimeChartController({ data, margin, axesConfig }: Props) { + + const [ rect, ref ] = useClientRect(); + + // we need to recalculate the axis scale whenever the rect width changes + // eslint-disable-next-line react-hooks/exhaustive-deps + const axisParams = React.useMemo(() => getAxisParams(data, axesConfig), [ data, axesConfig, rect?.width ]); + + const chartMargin = React.useMemo(() => { + const exceedingDigits = (axisParams.y.labelFormatParams.maximumSignificantDigits ?? DEFAULT_MAXIMUM_SIGNIFICANT_DIGITS) - + DEFAULT_MAXIMUM_SIGNIFICANT_DIGITS; + const PIXELS_PER_DIGIT = 7; + const leftShift = PIXELS_PER_DIGIT * exceedingDigits; + + return { + ...margin, + left: (margin?.left ?? 0) + leftShift, + }; + }, [ axisParams.y.labelFormatParams.maximumSignificantDigits, margin ]); + + const { innerWidth, innerHeight } = calculateInnerSize(rect, chartMargin); + + const xScale = React.useMemo(() => { + return axisParams.x.scale.range([ 0, innerWidth ]); + }, [ axisParams.x.scale, innerWidth ]); + + const yScale = React.useMemo(() => { + return axisParams.y.scale.range([ innerHeight, 0 ]); + }, [ axisParams.y.scale, innerHeight ]); + + return React.useMemo(() => { + return { + rect, + ref, + chartMargin, + innerWidth, + innerHeight, + axis: { + x: { + tickFormatter: axisParams.x.tickFormatter, + scale: xScale, + }, + y: { + tickFormatter: axisParams.y.tickFormatter, + scale: yScale, + }, + }, + }; + }, [ axisParams.x.tickFormatter, axisParams.y.tickFormatter, chartMargin, innerHeight, innerWidth, rect, ref, xScale, yScale ]); } diff --git a/ui/shared/chart/utils/timeChartAxis.ts b/ui/shared/chart/utils/timeChartAxis.ts new file mode 100644 index 0000000000..c9053fa18d --- /dev/null +++ b/ui/shared/chart/utils/timeChartAxis.ts @@ -0,0 +1,99 @@ +import * as d3 from 'd3'; +import _unique from 'lodash/uniq'; + +import type { AxesConfig, AxisConfig, TimeChartData } from '../types'; + +import { WEEK, MONTH, YEAR } from 'lib/consts'; + +export const DEFAULT_MAXIMUM_SIGNIFICANT_DIGITS = 2; +export const DEFAULT_MAXIMUM_FRACTION_DIGITS = 3; +export const MAXIMUM_SIGNIFICANT_DIGITS_LIMIT = 8; + +export function getAxisParams(data: TimeChartData, axesConfig?: AxesConfig) { + const { labelFormatParams: labelFormatParamsY, scale: yScale } = getAxisParamsY(data, axesConfig?.y); + + return { + x: { + scale: getAxisParamsX(data).scale, + tickFormatter: tickFormatterX, + }, + y: { + scale: yScale, + labelFormatParams: labelFormatParamsY, + tickFormatter: getTickFormatterY(labelFormatParamsY), + }, + }; +} + +function getAxisParamsX(data: TimeChartData) { + const min = d3.min(data, ({ items }) => d3.min(items, ({ date }) => date)) ?? new Date(); + const max = d3.max(data, ({ items }) => d3.max(items, ({ date }) => date)) ?? new Date(); + const scale = d3.scaleTime().domain([ min, max ]); + + return { min, max, scale }; +} + +const tickFormatterX = (axis: d3.Axis) => (d: d3.AxisDomain) => { + let format: (date: Date) => string; + const scale = axis.scale(); + const extent = scale.domain(); + + const span = Number(extent[1]) - Number(extent[0]); + + if (span > YEAR) { + format = d3.timeFormat('%Y'); + } else if (span > 2 * MONTH) { + format = d3.timeFormat('%b'); + } else if (span > WEEK) { + format = d3.timeFormat('%b %d'); + } else { + format = d3.timeFormat('%a %d'); + } + + return format(d as Date); +}; + +function getAxisParamsY(data: TimeChartData, config?: AxisConfig) { + const DEFAULT_TICKS_NUM = 3; + const min = d3.min(data, ({ items }) => d3.min(items, ({ value }) => value)) ?? 0; + const max = d3.max(data, ({ items }) => d3.max(items, ({ value }) => value)) ?? 0; + const scale = config?.nice ? + d3.scaleLinear() + .domain([ min, max ]) + .nice(config?.ticks ?? DEFAULT_TICKS_NUM) : + d3.scaleLinear() + .domain([ min, max ]); + + const ticks = scale.ticks(config?.ticks ?? DEFAULT_TICKS_NUM); + const labelFormatParams = getYLabelFormatParams(ticks); + + return { min, max, scale, labelFormatParams }; +} + +const getTickFormatterY = (params: Intl.NumberFormatOptions) => () => (d: d3.AxisDomain) => { + const num = Number(d); + + if (num < 1) { + // for small number there are no algorithm to format label right now + // so we set it to 3 digits after dot maximum + return num.toLocaleString(undefined, { maximumFractionDigits: 3 }); + } + + return num.toLocaleString(undefined, params); +}; + +function getYLabelFormatParams(ticks: Array, maximumSignificantDigits = DEFAULT_MAXIMUM_SIGNIFICANT_DIGITS): Intl.NumberFormatOptions { + const params = { + maximumFractionDigits: 3, + maximumSignificantDigits, + notation: 'compact' as const, + }; + + const uniqTicksStr = _unique(ticks.map((tick) => tick.toLocaleString(undefined, params))); + + if (uniqTicksStr.length === ticks.length || maximumSignificantDigits === MAXIMUM_SIGNIFICANT_DIGITS_LIMIT) { + return params; + } + + return getYLabelFormatParams(ticks, maximumSignificantDigits + 1); +}