/** * WordPress dependencies */ import { useSelect } from '@safe-wordpress/data'; import { _x, sprintf } from '@safe-wordpress/i18n'; /** * External dependencies */ import { Line } from 'react-chartjs-2'; import ChartDataLabels from 'chartjs-plugin-datalabels'; import 'chartjs-adapter-moment'; import { Chart as ChartJS, BarElement, CategoryScale, Legend, LineElement, LinearScale, PointElement, TimeScale, Title, Tooltip, } from 'chart.js'; ChartJS.register( BarElement, CategoryScale, ChartDataLabels, Legend, LineElement, LinearScale, PointElement, TimeScale, Title, Tooltip ); import { store as NAB_DATA } from '@nab/data'; import { COLORS, aggregateResultsByTimeGrouping, getLetter } from '@nab/utils'; import type { ChartDataset, ScatterDataPoint } from 'chart.js'; import type { AlternativeTrackingData, ECommercePlugin, GoalId, Maybe, } from '@nab/types'; /** * Internal dependencies */ import { getEvolutionChartOptions } from './helpers'; import { getMoneyLabel } from '../utils'; export type RevenueTimelineChartProps = { readonly alternatives: Maybe< ReadonlyArray< AlternativeTrackingData > >; readonly firstDayOfWeek: number; readonly goal: GoalId; readonly unique: boolean; readonly timeGrouping: 'daily' | 'weekly' | 'monthly'; readonly winner: Maybe< number >; }; export const RevenueTimelineChart = ( { alternatives = [], firstDayOfWeek, goal, unique, winner, timeGrouping, }: RevenueTimelineChartProps ): JSX.Element => { const ecommerce = useECommercePlugin( goal ); const currency = useCurrencySymbol( ecommerce ); const colors = [ COLORS.nabBackgroundAlternativeA, COLORS.nabBackgroundAlternativeB, COLORS.nabBackgroundAlternativeC, COLORS.nabBackgroundAlternativeD, COLORS.nabBackgroundAlternativeE, COLORS.nabBackgroundAlternativeF, ]; const pointStyles = [ { type: 'circle' as const, size: 2.5 }, { type: 'rect' as const, size: 3 }, { type: 'triangle' as const, size: 3.5 }, ]; const lines = alternatives.map( ( a ) => getData( a, goal, unique, timeGrouping, firstDayOfWeek ) ); const datasets: ChartDataset[] = lines.map( ( l, i ) => ( { label: i === 0 ? _x( 'Variant A (Control)', 'text', 'nelio-ab-testing' ) : sprintf( /* translators: %s: Variant letter. */ _x( 'Variant %s', 'text', 'nelio-ab-testing' ), getLetter( i ) ), backgroundColor: colors[ i % colors.length ], borderColor: colors[ i % colors.length ], fill: false, tension: 0.4, borderWidth: i === winner ? 3 : 2, pointStyle: pointStyles[ i % pointStyles.length ]?.type, pointRadius: ( pointStyles[ i % pointStyles.length ]?.size ?? 3 ) * ( i === winner ? 1.2 : 1 ), pointHitRadius: 4, pointHoverRadius: 4, data: l, } ) ); const options = getEvolutionChartOptions( { timeGrouping, title: _x( 'Accumulated Revenue', 'text', 'nelio-ab-testing' ) + ` (${ currency })`, tooltip: ( context ) => sprintf( '%1$s: %2$s', context.dataset.label, getMoneyLabel( context.raw.y || 0, ecommerce ) ), } ); // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-assignment return ; }; // ======= // HELPERS // ======= const getData = ( alternative: Maybe< AlternativeTrackingData >, goal: GoalId, unique: boolean, timeGrouping: 'daily' | 'weekly' | 'monthly', firstDayOfWeek: number ): ScatterDataPoint[] => { if ( ! alternative ) { return []; } let acc = 0; return aggregateResultsByTimeGrouping( alternative.timeline, timeGrouping, firstDayOfWeek ).map( ( day ) => { acc += unique ? day.uniqueValues[ goal ] || 0 : day.values[ goal ] || 0; return { x: day.date, y: acc, } as unknown as ScatterDataPoint; } ); }; // ===== // HOOKS // ===== const useECommercePlugin = ( goal: GoalId ) => useSelect( ( select ) => { const { getPageAttribute, getECommercePlugin } = select( NAB_DATA ); const activeExperiment = getPageAttribute( 'editor/activeExperiment' ) ?? 0; return getECommercePlugin( activeExperiment, goal ); }, [ goal ] ); const useCurrencySymbol = ( ecommerce: ECommercePlugin = 'woocommerce' ) => useSelect( ( select ) => select( NAB_DATA ).getECommerceSetting( ecommerce, 'currencySymbol' ), [ ecommerce ] );