import {Line} from 'react-chartjs-2'; import { Chart as ChartJS, CategoryScale, LinearScale, PointElement, LineElement, Title, Tooltip, Legend } from 'chart.js'; import {__} from '@wordpress/i18n'; import { useMemo } from 'react'; import {alignCompareSeries} from './compareMath'; import {formatBucketLabel} from './dateLabels'; ChartJS.register( CategoryScale, LinearScale, PointElement, LineElement, Title, Tooltip, Legend ); const rgba = ( hex, a = 1 ) => `rgba(${parseInt( hex.slice( 1 ), 16 ) >> 16 & 255}, ${parseInt( hex.slice( 1 ), 16 ) >> 8 & 255}, ${parseInt( hex.slice( 1 ), 16 ) & 255}, ${a})`; const METRIC_STYLES = { impressions: { label: __( 'Impressions', 'adpresso' ), borderColor: rgba( '#3B82B6' ), backgroundColor: rgba( '#3B82B6', 0.5 ), yAxisID: 'yLeft' // Standard: linke Achse }, clicks: { label: __( 'Clicks', 'adpresso' ), borderColor: rgba( '#A78C2D' ), backgroundColor: rgba( '#A78C2D', 0.5 ), yAxisID: 'yLeft' }, ctr: { label: __( 'CTR (%)', 'adpresso' ), borderColor: rgba( '#34875C' ), backgroundColor: rgba( '#34875C', 0.5 ), yAxisID: 'yRight' // Rate-Metriken auf die rechte Achse }, viewability: { label: __( 'Active View Rate (%)', 'adpresso' ), borderColor: rgba( '#62499C' ), backgroundColor: rgba( '#62499C', 0.5 ), yAxisID: 'yRight', premium: true }, impressions_av: { label: __( 'Active View Impressions', 'adpresso' ), borderColor: rgba( '#9E3B32' ), backgroundColor: rgba( '#9E3B32', 0.5 ), yAxisID: 'yLeft', premium: true } }; const {dateFormat} = window.adpressoAnalyticsData || {dateFormat: 'F j, Y'}; const formatValue = ( value, metricKey ) => { if ( value === null || value === undefined ) { return '-'; } return METRIC_STYLES[metricKey]?.yAxisID === 'yRight' ? `${Number( value ).toFixed( 2 )}%` : new Intl.NumberFormat( 'default' ).format( value ); }; export const tooltipLabel = ( ctx ) => { const {dataset, dataIndex, parsed} = ctx; const value = formatValue( parsed.y, dataset.metricKey ); // Compare points carry their own date; the tooltip title is the main date. const date = dataset.pointDates ? dataset.pointDates[dataIndex] : null; return date ? `${dataset.label} (${date}): ${value}` : `${dataset.label}: ${value}`; }; const ChartRenderer = ( {chartData, visibleMetrics, compareData = null, compareMode = null, grouping = 'day', label = ''} ) => { const datasets = []; const formatLabel = ( date ) => formatBucketLabel( date, grouping, dateFormat ); const labels = chartData.map( item => formatLabel( item.date ) ); const hasCompare = Array.isArray( compareData ) && compareData.length > 0; // Day 1 of the compare range sits under day 1 of the main range. const alignedCompare = hasCompare ? alignCompareSeries( chartData, compareData ) : []; const compareSuffix = compareMode === 'yoy' ? __( 'Previous year', 'adpresso' ) : __( 'Previous period', 'adpresso' ); visibleMetrics.forEach( key => { if ( !METRIC_STYLES[key] ) { return; } datasets.push( { ...METRIC_STYLES[key], metricKey: key, data: chartData.map( item => item[key] ) } ); if ( hasCompare ) { datasets.push( { ...METRIC_STYLES[key], metricKey: key, isCompare: true, label: `${METRIC_STYLES[key].label} (${compareSuffix})`, data: alignedCompare.map( item => ( item ? item[key] : null ) ), pointDates: alignedCompare.map( item => ( item ? formatLabel( item.date ) : null ) ), borderColor: METRIC_STYLES[key].backgroundColor, backgroundColor: 'transparent', borderDash: [5, 5], pointRadius: 2 } ); } } ); const scales = { x: { ticks: { autoSkip: true, // Überspringt automatisch Labels, wenn es zu eng wird maxTicksLimit: 25, // Zeigt maximal 15 Datumswerte an (der Rest wird ausgeblendet, Linie bleibt aber präzise) maxRotation: 45, // Erzwingt einen schönen 45-Grad-Winkel (verhindert wildes Hin- und Herspringen) minRotation: 45, padding: 10 // Gibt den Labels etwas Luft nach oben zur X-Achse }, grid: { display: false // Optional: Blendet die vertikalen Gitterlinien aus, macht den Chart viel ruhiger } }, yLeft: { type: 'linear', display: true, position: 'left', title: {display: true, text: __( 'Count', 'adpresso' )} }, yRight: { type: 'linear', display: false, position: 'right', title: {display: true, text: __( 'Rate (%)', 'adpresso' )}, grid: {drawOnChartArea: false} } }; // Prüfen, welche Arten von Metriken aktuell ausgewählt sind const hasCountMetric = visibleMetrics.some( key => METRIC_STYLES[key]?.yAxisID === 'yLeft' ); const hasRateMetric = visibleMetrics.some( key => METRIC_STYLES[key]?.yAxisID === 'yRight' ); // Skalen streng nach METRIC_STYLES ein- oder ausblenden scales.yLeft.display = hasCountMetric; scales.yRight.display = hasRateMetric; // UX-Verbesserung: Wenn NUR Prozentwerte gewählt sind, // schieben wir die rechte Achse optisch nach links, damit der Chart dort nicht "leer" ist. if ( !hasCountMetric && hasRateMetric ) { scales.yRight.position = 'left'; } else { scales.yRight.position = 'right'; } const data = useMemo(() => ({ labels: labels, datasets: datasets }), [labels, datasets]); const options = useMemo(() => ({ responsive: true, maintainAspectRatio: false, interaction: {mode: 'index', intersect: false}, plugins: { // The metric toggles act as legend; with a compare series a legend tells solid and dashed apart. legend: hasCompare ? {display: true, position: 'bottom'} : false, title: {display: false, text: __( 'Performance Overview', 'adpresso' )}, tooltip: {callbacks: {label: tooltipLabel}} }, scales: scales }), [visibleMetrics, hasCompare]); const ariaLabel = hasCompare ? __( 'Performance chart with a dashed comparison series. The same data is listed in the table below.', 'adpresso' ) : __( 'Performance chart. The same data is listed in the table below.', 'adpresso' ); return ; }; export default ChartRenderer;