From 3029a796b87e3b4b8afbf3551f25d2e03a2e4258 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rados=C5=82aw=20Szymkiewicz?= Date: Wed, 8 May 2024 19:44:13 +0200 Subject: [PATCH] Improvements in charts --- package.json | 2 +- src/admin/routes/analytics/page.tsx | 2 +- src/ui-components/common/chart-components.tsx | 200 ++++++++++++------ .../common/overview-components.tsx | 2 +- src/ui-components/common/utils/chartUtils.ts | 66 +++--- .../order-frequency-distribution-chart.tsx | 6 +- .../orders/orders-payment-provider-chart.tsx | 6 +- src/ui-components/utils/helpers.ts | 16 +- src/ui-components/utils/types.ts | 6 +- 9 files changed, 181 insertions(+), 125 deletions(-) diff --git a/package.json b/package.json index 37d7ea0..666006f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@rsc-labs/medusa-store-analytics", - "version": "0.12.1", + "version": "0.12.2", "description": "Get analytics data about your store", "author": "RSC Labs (https://rsoftcon.com)", "main": "dist/index.js", diff --git a/src/admin/routes/analytics/page.tsx b/src/admin/routes/analytics/page.tsx index 3111105..24dc59b 100644 --- a/src/admin/routes/analytics/page.tsx +++ b/src/admin/routes/analytics/page.tsx @@ -13,7 +13,7 @@ import { useState } from 'react'; import { useMemo } from "react" import { RouteConfig } from "@medusajs/admin" -import { Tabs } from "@medusajs/ui" +import { Tabs, Text } from "@medusajs/ui" import { LightBulb } from "@medusajs/icons" import { Box } from "@mui/material"; import OverviewTab from "../../../ui-components/tabs/overview"; diff --git a/src/ui-components/common/chart-components.tsx b/src/ui-components/common/chart-components.tsx index 0f6b9c4..ec5b6d9 100644 --- a/src/ui-components/common/chart-components.tsx +++ b/src/ui-components/common/chart-components.tsx @@ -10,11 +10,22 @@ * limitations under the License. */ -import { Heading, Container } from "@medusajs/ui"; -import { calculateResolution, getChartDateName, getChartTooltipDate, getLegendName } from "./utils/chartUtils"; -import { ChartResolutionType } from "./utils/chartUtils"; -import { AreaChart, Area, XAxis, YAxis, CartesianGrid, Tooltip, Legend } from 'recharts'; +import { Heading, Container, Text } from "@medusajs/ui"; +import { calculateResolution, getChartDateName, getChartTooltipDate, getLegendName, ChartResolutionType, compareDatesBasedOnResolutionType } from "./utils/chartUtils"; +import { AreaChart, Area, XAxis, YAxis, CartesianGrid, Tooltip, Legend} from 'recharts'; import { useEffect, useState } from 'react'; +import { Box, Grid } from "@mui/material"; + +type ChartDataPoint = { + current: { + date: Date, + value: any + }, + previous: { + date: Date, + value: any + } +} export type ChartDataType = { current: { @@ -27,23 +38,8 @@ export type ChartDataType = { }[] } -const compareDatesBasedOnResolutionType = (date1: Date, date2: Date, resolutionType: ChartResolutionType): boolean => { - switch (resolutionType) { - case ChartResolutionType.DayWeek: - case ChartResolutionType.DayMonth: - return new Date(new Date(date1).setHours(0,0,0,0)).getTime() == new Date(new Date(date2).setHours(0,0,0,0)).getTime(); - case ChartResolutionType.Month: - return new Date(new Date(new Date(date1).setDate(0)).setHours(0,0,0,0)).getTime() == new Date(new Date(new Date(date2).setDate(0)).setHours(0,0,0,0)).getTime(); - default: - return new Date(new Date(date1).setHours(0,0,0,0)).getTime() == new Date(new Date(date2).setHours(0,0,0,0)).getTime(); - } -} - const incrementDate = (date: Date, resolutionType: ChartResolutionType) => { switch (resolutionType) { - case ChartResolutionType.DayWeek: - date.setDate(date.getDate() + 1); - break; case ChartResolutionType.DayMonth: date.setDate(date.getDate() + 1); break; @@ -55,22 +51,28 @@ const incrementDate = (date: Date, resolutionType: ChartResolutionType) => { } }; -export const generateChartData = (data: ChartDataType, startFrom: Date, endAt: Date, chartResolutionType: ChartResolutionType, connectEmptyPointsUsingPreviousValue?: boolean) => { +const generateChartData = ( + data: ChartDataType, + fromDate: Date, + toDate: Date, + chartResolutionType: ChartResolutionType, + toCompareDate?: Date, + connectEmptyPointsUsingPreviousValue?: boolean) + : ChartDataPoint[] => { const currentData = data.current; const previousData = data.previous; - const currentDate = new Date(startFrom); - const offsetTime = endAt.getTime() - startFrom.getTime(); - - const dataPoints = []; + const startFromDate = new Date(fromDate); + const offsetTime = toDate.getTime() - (toCompareDate ? toCompareDate.getTime() : fromDate.getTime()); + const dataPoints: ChartDataPoint[] = []; let currentDataValue: any; let previousDataValue: any; - while (currentDate.getTime() < endAt.getTime() || compareDatesBasedOnResolutionType(currentDate, endAt, chartResolutionType)) { - const currentOrder = currentData.find(order => compareDatesBasedOnResolutionType(new Date(order.date), currentDate, chartResolutionType)); - const offsetDate = new Date(currentDate); + while (startFromDate.getTime() < toDate.getTime() || compareDatesBasedOnResolutionType(startFromDate, toDate, chartResolutionType)) { + const currentOrder = currentData.find(order => compareDatesBasedOnResolutionType(new Date(order.date), startFromDate, chartResolutionType)); + const offsetDate = new Date(startFromDate); offsetDate.setTime(offsetDate.getTime() - offsetTime); const previousOrder = previousData.find(previous => compareDatesBasedOnResolutionType(new Date(previous.date), offsetDate, chartResolutionType)); @@ -83,67 +85,63 @@ export const generateChartData = (data: ChartDataType, startFrom: Date, endAt: D } dataPoints.push({ - date: new Date(currentDate), - current: currentOrder ? parseInt(currentOrder.value) : (currentDataValue ? currentDataValue : undefined), - previous: previousOrder ? parseInt(previousOrder.value) : (previousDataValue ? previousDataValue : undefined), + current: { + date: new Date(startFromDate), + value: currentOrder ? parseInt(currentOrder.value) : (currentDataValue ? currentDataValue : undefined), + }, + previous: { + date: new Date(offsetDate), + value: previousOrder ? parseInt(previousOrder.value) : (previousDataValue ? previousDataValue : undefined), + } }); } else { dataPoints.push({ - date: new Date(currentDate), - current: currentOrder ? parseInt(currentOrder.value) : 0, - previous: previousOrder ? parseInt(previousOrder.value) : 0, + current: { + date: new Date(startFromDate), + value: currentOrder ? parseInt(currentOrder.value) : 0 + }, + previous: { + date: new Date(offsetDate), + value: previousOrder ? parseInt(previousOrder.value) : 0, + } }); } - incrementDate(currentDate, chartResolutionType); + incrementDate(startFromDate, chartResolutionType); } if (connectEmptyPointsUsingPreviousValue) { for (let i = dataPoints.length - 1; i >= 0; i--) { - if (dataPoints[i].current === undefined) { - if (dataPoints[dataPoints.length - 1].previous) { - dataPoints[i].current = dataPoints[dataPoints.length - 1].previous + if (dataPoints[i].current.value === undefined) { + if (dataPoints[dataPoints.length - 1].previous.value) { + dataPoints[i].current.value = dataPoints[dataPoints.length - 1].previous.value } else { - dataPoints[i].current = 0; + dataPoints[i].current.value = 0; } } - if (dataPoints[i].previous) { - previousDataValue = dataPoints[i].previous + if (dataPoints[i].previous.value) { + previousDataValue = dataPoints[i].previous.value } else { - dataPoints[i].previous = previousDataValue; + dataPoints[i].previous.value = previousDataValue; } } } - return dataPoints; } export const ChartCustomTooltip = ({ active, payload, label, resolutionType }) => { if (active && payload && payload.length) { switch (resolutionType) { - case ChartResolutionType.DayWeek: - return ( - - - {`Current ${getChartTooltipDate(payload[0].payload.date, resolutionType)}`} : {payload[0].value} - - {payload[1] !== undefined && - - {`Previous ${getChartTooltipDate(payload[1].payload.date, resolutionType)}`} : {payload[1].value} - - } - - ) case ChartResolutionType.DayMonth: return ( - {`${getChartTooltipDate(payload[0].payload.date, resolutionType)}`} : {payload[0].value} + {`${getChartTooltipDate(payload[0].payload.current.date, resolutionType)}`} : {payload[0].payload.current.value} {payload[1] !== undefined && - {`${getChartTooltipDate(new Date(new Date(payload[1].payload.date).setMonth(payload[1].payload.date.getMonth() - 1)), resolutionType)}`} : {payload[1].value} + {`${getChartTooltipDate(payload[1].payload.previous.date, resolutionType)}`} : {payload[1].payload.previous.value} } @@ -152,11 +150,11 @@ export const ChartCustomTooltip = ({ active, payload, label, resolutionType }) = return ( - {`${getChartTooltipDate(payload[0].payload.date, resolutionType)}`} : {payload[0].value} + {`${getChartTooltipDate(payload[0].payload.current.date, resolutionType)}`} : {payload[0].payload.current.value} {payload[1] !== undefined && - {`${getChartTooltipDate(new Date(new Date(payload[1].payload.date).setFullYear(payload[1].payload.date.getFullYear() - 1)), resolutionType)}`} : {payload[1].value} + {`${getChartTooltipDate(payload[1].payload.previous.date, resolutionType)}`} : {payload[1].payload.previous.value} } @@ -167,30 +165,96 @@ export const ChartCustomTooltip = ({ active, payload, label, resolutionType }) = return null; }; +/* + +toDate is inclusive. It means that: + fromDate: "2024-04-24" + toDate: "2024-04-30" + + Analytics shall include `toDate` so it takes 7 days (including 2024-04-30) + + fromCompareDate: "2024-04-17" + toCompareDate: "2024-04-24" + + Analytics shall compare to 7 days excluding 2024-04-24 (e.g. 2024-04-30 is compared to 2024-04-23, not 2024-04-24). + + toDate is inclusive to cover "today" date - so we need to cover situation when someone wants to see everything until now. + We cannot use 2024-05-01 because then it is taken as day to show, while we want to show maximum 2024-04-30. + + toCompareDate is exclusive because backend is using fetches like created_at < toCompareDate, so it does not cover data at toCompareDate + + Comparison then we will have following algorithm: + 1) Take "toDate", remove "time" part and add whole day. + 2) Take times in milis from every date and compare. +*/ + +const areRangesTheSame = (fromDate: Date, toDate: Date, fromCompareDate?: Date, toCompareDate?: Date) : boolean => { + + if (fromCompareDate) { + const oneDay = 24 * 60 * 60 * 1000; + if (toCompareDate) { + // Math.ceil is used to round the day to larger value for taking the whole day for comparison + const diffBase = Math.ceil(Math.abs((toDate.getTime() - fromDate.getTime()) / oneDay)); + const diffCompare = Math.ceil(Math.abs((toCompareDate.getTime() - fromCompareDate.getTime()) / oneDay)); + return (diffBase == diffCompare); + } + + const diffBase = Math.ceil(Math.abs((toDate.getTime() - fromDate.getTime()) / oneDay)); + const diffCompare = Math.ceil(Math.abs((Date.now() - fromCompareDate.getTime()) / oneDay)); + + return (diffBase == diffCompare); + } + return true; +}; + export const ChartCurrentPrevious = ({rawChartData, fromDate, toDate, fromCompareDate, toCompareDate, compareEnabled, connectEmptyPointsUsingPreviousValue} : { rawChartData: ChartDataType, fromDate: Date, toDate: Date, fromCompareDate?: Date, toCompareDate?: Date, compareEnabled?: boolean, connectEmptyPointsUsingPreviousValue?: boolean}) => { - const [chartData, setChartData] = useState([]); + const [chartDataPoints, setChartData] = useState([]); - const resolutionType = calculateResolution(fromDate); + const resolutionType = calculateResolution(fromDate, toDate); useEffect(() => { - const dataPoints = generateChartData( + const chartDataPoints: ChartDataPoint[] = generateChartData( rawChartData, fromDate, toDate, resolutionType, + toCompareDate, connectEmptyPointsUsingPreviousValue ); - setChartData(dataPoints); - + setChartData(chartDataPoints); + }, [rawChartData, fromDate, toDate]); + if (!areRangesTheSame(fromDate, toDate, fromCompareDate, toCompareDate)) { + const currentPeriodInDays = Math.ceil((toDate.getTime() - fromDate.getTime()) / (24*60*60*1000)); + const precedingPeriodInDays = Math.ceil((toCompareDate.getTime() - fromCompareDate.getTime()) / (24*60*60*1000)); + return ( + + + + Chart can be shown only for the same length of ranges. + + + {`You are comparing ${currentPeriodInDays} days to ${precedingPeriodInDays} days`} + + + + ) + } + return ( - getChartDateName(value.date, resolutionType)}/> + getChartDateName(value.current.date, resolutionType, fromDate, toDate)} minTickGap={15} interval={'preserveStartEnd'}/> } /> - {} - {(compareEnabled && fromCompareDate) && } + {} + {(compareEnabled && fromCompareDate) && } {(compareEnabled && fromCompareDate) && } ) diff --git a/src/ui-components/common/overview-components.tsx b/src/ui-components/common/overview-components.tsx index 71aef68..4b4eb5c 100644 --- a/src/ui-components/common/overview-components.tsx +++ b/src/ui-components/common/overview-components.tsx @@ -83,7 +83,7 @@ export const DropdownOrderStatus = ({onOrderStatusChange, appliedStatuses} : {on {Object.values(OrderStatus).map(orderStatus => ( - event.preventDefault()}> + event.preventDefault()} key={orderStatus.toString()}> = weekAgoTruncated) { - return ChartResolutionType.DayWeek; - } + const calculateToDate = toDate ? new Date(toDate) : new Date(Date.now()); + const diffTime = calculateToDate.getTime() - fromDate.getTime(); - const monthAgoTruncated = new Date(new Date(new Date().setMonth(new Date().getMonth() - 1)).setHours(0,0,0,0)); - if (date >= monthAgoTruncated) { + const weekTime = 604800000; + const monthTime = weekTime * 4; + const twoMonthsTime = monthTime * 2; + if (diffTime <= twoMonthsTime) { return ChartResolutionType.DayMonth; } - const yearAgoTruncated = new Date(new Date(new Date().setFullYear(new Date().getFullYear() - 1)).setHours(0,0,0,0)); - if (date > yearAgoTruncated) { + const yearTime = monthTime * 12; + if (diffTime < yearTime) { return ChartResolutionType.Month; } return ChartResolutionType.Month } -export const getChartDateName = (date: Date, resolutionType: ChartResolutionType): string => { +export const compareDatesBasedOnResolutionType = (date1: Date, date2: Date, resolutionType: ChartResolutionType): boolean => { switch (resolutionType) { - case ChartResolutionType.DayWeek: - return getShortDayName(date); case ChartResolutionType.DayMonth: - return date.getDate().toString(); + return new Date(new Date(date1).setHours(0,0,0,0)).getTime() == new Date(new Date(date2).setHours(0,0,0,0)).getTime(); case ChartResolutionType.Month: - return getShortMonthName(date); + return new Date(new Date(new Date(date1).setDate(0)).setHours(0,0,0,0)).getTime() == new Date(new Date(new Date(date2).setDate(0)).setHours(0,0,0,0)).getTime(); default: - return date.getFullYear().toString() + return new Date(new Date(date1).setHours(0,0,0,0)).getTime() == new Date(new Date(date2).setHours(0,0,0,0)).getTime(); } } -export const getChartTooltipDate = (date: Date, resolutionType: ChartResolutionType): string => { +export const getChartDateName = (date: Date, resolutionType: ChartResolutionType, startDate: Date, endDate: Date): string => { + switch (resolutionType) { - case ChartResolutionType.DayWeek: - return getFullDayName(date); case ChartResolutionType.DayMonth: - return `${date.getDate().toString()}-${getShortMonthName(date)}`; + if (compareDatesBasedOnResolutionType(date, startDate, resolutionType) || compareDatesBasedOnResolutionType(date, endDate, resolutionType)) { + return `${date.getDate().toString()} ${getShortMonthName(date)}`; + } + return date.getDate().toString(); case ChartResolutionType.Month: - return `${getShortMonthName(date)}-${date.getFullYear()}`; + if (compareDatesBasedOnResolutionType(date, startDate, resolutionType) || compareDatesBasedOnResolutionType(date, endDate, resolutionType)) { + return `${getShortMonthName(date)} ${date.getFullYear().toString()}`; + } + return getShortMonthName(date); default: return date.getFullYear().toString() } } -export const getLegendName = (resolutionType: ChartResolutionType, current: boolean): string => { +export const getChartTooltipDate = (date: Date, resolutionType: ChartResolutionType): string => { switch (resolutionType) { - case ChartResolutionType.DayWeek: - return current ? `Current week` : `Previous week`; case ChartResolutionType.DayMonth: - return current ? `Current month` : `Previous month`; + return `${date.getDate().toString()}-${getShortMonthName(date)}`; case ChartResolutionType.Month: - return current ? `Current year` : `Previous year`; + return `${getShortMonthName(date)}-${date.getFullYear()}`; + default: + return date.getFullYear().toString() } } -const getFullDayName = (date: Date) => { - let days = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday']; - return days[date.getDay()]; -} - -const getShortDayName = (date: Date) => { - let days = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']; - return days[date.getDay()]; +export const getLegendName = (current: boolean): string => { + return current ? `Current` : `Preceding`; } const getShortMonthName = (date: Date) => { diff --git a/src/ui-components/customers/repeat-customer-rate/order-frequency-distribution-chart.tsx b/src/ui-components/customers/repeat-customer-rate/order-frequency-distribution-chart.tsx index 036c9d7..95136d1 100644 --- a/src/ui-components/customers/repeat-customer-rate/order-frequency-distribution-chart.tsx +++ b/src/ui-components/customers/repeat-customer-rate/order-frequency-distribution-chart.tsx @@ -44,8 +44,6 @@ export const OrderFrequencyDistributionPieChart = ({repeatCustomerRateResponse, const currentData = convertToChartData(repeatCustomerRateResponse.analytics.current); const previousData = convertToChartData(repeatCustomerRateResponse.analytics.previous); - const resolutionType = calculateResolution(new Date(repeatCustomerRateResponse.analytics.dateRangeFrom)); - const renderLabel = function(entry) { return entry.displayValue; } @@ -58,11 +56,11 @@ export const OrderFrequencyDistributionPieChart = ({repeatCustomerRateResponse, } {(compareEnabled && repeatCustomerRateResponse.analytics.dateRangeFromCompareTo) && } diff --git a/src/ui-components/orders/orders-payment-provider-chart.tsx b/src/ui-components/orders/orders-payment-provider-chart.tsx index f60343b..4f23447 100644 --- a/src/ui-components/orders/orders-payment-provider-chart.tsx +++ b/src/ui-components/orders/orders-payment-provider-chart.tsx @@ -46,8 +46,6 @@ export const OrdersPaymentProviderPieChart = ({ordersPaymentProviderResponse, co const currentData = convertToChartData(ordersPaymentProviderResponse.analytics.current); const previousData = convertToChartData(ordersPaymentProviderResponse.analytics.previous); - const resolutionType = calculateResolution(new Date(ordersPaymentProviderResponse.analytics.dateRangeFrom)); - const renderLabel = function(entry) { return entry.displayValue; } @@ -60,11 +58,11 @@ export const OrdersPaymentProviderPieChart = ({ordersPaymentProviderResponse, co } {(compareEnabled && ordersPaymentProviderResponse.analytics.dateRangeFromCompareTo) && } diff --git a/src/ui-components/utils/helpers.ts b/src/ui-components/utils/helpers.ts index 4436cd3..bb16f0a 100644 --- a/src/ui-components/utils/helpers.ts +++ b/src/ui-components/utils/helpers.ts @@ -40,14 +40,14 @@ export function convertDateLastsToDateRange(dateLasts: DateLasts): DateRange | u switch (dateLasts) { case DateLasts.LastMonth: result = { - // 86400000 - alignment for taking last 6 days, as the current day is 7th - from: new Date(new Date(new Date().setMonth(new Date().getMonth() - 1) + 86400000).setHours(0,0,0,0)), + // 86400000 - alignment for taking last 29 days, as the current day is 30 + from: new Date(new Date(new Date().setDate(new Date().getDate() - 29)).setHours(0,0,0,0)), to: new Date(Date.now()) } break; case DateLasts.LastWeek: result = { - // 86400000 - alignment for taking last 6 days, as the current day is 7th + // 86400000 - alignment for taking last 6 days, as the current day is 7th from: new Date(new Date(new Date(Date.now() - 604800000 + 86400000)).setHours(0,0,0,0)), to: new Date(Date.now()) } @@ -56,7 +56,7 @@ export function convertDateLastsToDateRange(dateLasts: DateLasts): DateRange | u const lastYearAgo = new Date(new Date().setFullYear(new Date().getFullYear() - 1)); result = { // + 1 - alignment for taking last 11 months, as the current month is 12th - from: new Date(new Date(new Date(lastYearAgo).setMonth(lastYearAgo.getMonth() + 1)).setHours(0,0,0,0) + 86400000), + from: new Date(new Date(new Date().setDate(new Date().getDate() - 364)).setHours(0,0,0,0)), to: new Date(Date.now()) } break; @@ -69,8 +69,8 @@ export function convertDateLastsToComparedDateRange(dateLasts: DateLasts): DateR switch (dateLasts) { case DateLasts.LastMonth: result = { - from: new Date(new Date(new Date().setMonth(new Date().getMonth() - 2)).setHours(0,0,0,0) + 86400000), - to: new Date(new Date(new Date().setMonth(new Date().getMonth() - 1)).setHours(0,0,0,0) + 86400000), + from: new Date(new Date(new Date().setDate(new Date().getDate() - 59)).setHours(0,0,0,0)), + to: new Date(new Date(new Date().setDate(new Date().getDate() - 29)).setHours(0,0,0,0)), } break; case DateLasts.LastWeek: @@ -81,8 +81,8 @@ export function convertDateLastsToComparedDateRange(dateLasts: DateLasts): DateR break; case DateLasts.LastYear: result = { - from: new Date(new Date(new Date().setFullYear(new Date().getFullYear() - 2)).setHours(0,0,0,0) + 86400000), - to: new Date(new Date(new Date().setFullYear(new Date().getFullYear() - 1)).setHours(0,0,0,0) + + 86400000) + from: new Date(new Date(new Date().setDate(new Date().getDate() - 729)).setHours(0,0,0,0)), + to: new Date(new Date(new Date().setDate(new Date().getDate() - 364)).setHours(0,0,0,0)), } break; } diff --git a/src/ui-components/utils/types.ts b/src/ui-components/utils/types.ts index 347ce37..45fe8e1 100644 --- a/src/ui-components/utils/types.ts +++ b/src/ui-components/utils/types.ts @@ -37,9 +37,9 @@ export enum OrderStatus { export enum DateLasts { All = "All time", - LastMonth = "Last month", - LastWeek = "Last week", - LastYear = "Last year" + LastMonth = "Last 30 days", + LastWeek = "Last 7 days", + LastYear = "Last 365 days" } export type DateRange = { from: Date,