/** * WordPress dependencies */ import { _x, sprintf } from '@safe-wordpress/i18n'; /** * External dependencies */ import { keys } from 'lodash'; import { formatNumber } from '@nab/i18n'; 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'; import { COLORS, aggregateResultsByTimeGrouping, getLetter } from '@nab/utils'; ChartJS.register( BarElement, CategoryScale, ChartDataLabels, Legend, LineElement, LinearScale, PointElement, TimeScale, Title, Tooltip ); import type { ChartDataset, ScatterDataPoint } from 'chart.js'; import type { AlternativeTrackingData, GoalId, Maybe } from '@nab/types'; /** * Internal dependencies */ import { getEvolutionChartOptions } from './helpers'; export type ViewsAndConversionsTimelineProps = { 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 ViewsAndConversionsTimelineChart = ( { alternatives = [], firstDayOfWeek, goal, unique, winner, timeGrouping, }: ViewsAndConversionsTimelineProps ): JSX.Element => { 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 ) => getConversions( a, goal, unique, timeGrouping, firstDayOfWeek ) ); const datasets: ChartDataset[] = [ { label: _x( 'Views', 'text', 'nelio-ab-testing' ), backgroundColor: COLORS.wpAdminThemeColorRgb, borderColor: COLORS.wpAdminThemeColorRgb, fill: false, tension: 0.4, pointStyle: pointStyles[ 0 ]?.type, pointRadius: pointStyles[ 0 ]?.size ?? 3, pointHitRadius: 4, pointHoverRadius: 4, data: getViews( alternatives, unique, timeGrouping, firstDayOfWeek ), }, ...lines.map( ( l, i ) => ( { label: i === 0 ? _x( 'Conversions A (Control)', 'text', 'nelio-ab-testing' ) : sprintf( /* translators: %s: Variant letter. */ _x( 'Conversions %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( 'Views and Conversions', 'text', 'nelio-ab-testing' ), tooltip: ( context ) => sprintf( '%1$s: %2$s%%', context.dataset.label, formatNumber( context.raw.y || 0 ) ), } ); // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-assignment return ; }; // ======= // HELPERS // ======= const getViews = ( alternatives: ReadonlyArray< AlternativeTrackingData >, unique: boolean, timeGrouping: 'daily' | 'weekly' | 'monthly', firstDayOfWeek: number ): ScatterDataPoint[] => { const viewsPerDate = {} as Record< string, number >; alternatives.forEach( ( a ) => aggregateResultsByTimeGrouping( a.timeline, timeGrouping, firstDayOfWeek ).forEach( ( day ) => { const existing = viewsPerDate[ day.date ] || 0; const views = unique ? day.uniqueVisits : day.visits; viewsPerDate[ day.date ] = existing + views; } ) ); const dates = keys( viewsPerDate ).sort(); return dates.map( ( date ): ScatterDataPoint => ( { x: date as unknown as number, y: viewsPerDate[ date ] || 0, } ) ); }; const getConversions = ( alternative: Maybe< AlternativeTrackingData >, goal: GoalId, unique: boolean, timeGrouping: 'daily' | 'weekly' | 'monthly', firstDayOfWeek: number ): ScatterDataPoint[] => { if ( ! alternative ) { return []; } return aggregateResultsByTimeGrouping( alternative.timeline, timeGrouping, firstDayOfWeek ).map( ( day ): ScatterDataPoint => ( { x: day.date as unknown as number, y: unique ? day.uniqueConversions[ goal ] || 0 : day.conversions[ goal ] || 0, } ) ); };