-
- {children}
-
+
+
+
+ {children}
);
}
-// Utility function to export all charts from a container as a ZIP
-export async function exportAllCharts(containerSelector, zipFilename = 'charts') {
- const container = document.querySelector(containerSelector);
- if (!container) return;
-
- const zip = new JSZip();
- const chartWrappers = container.querySelectorAll('.exportable-chart-wrapper');
-
- for (let i = 0; i < chartWrappers.length; i++) {
- const wrapper = chartWrappers[i];
- const canvas = wrapper.querySelector('canvas');
- const titleEl = wrapper.querySelector('.chart-header-with-export h2');
- const title = titleEl?.textContent || `chart-${i + 1}`;
-
- if (!canvas) continue;
-
- // Create export canvas with white background and title
- const exportCanvas = document.createElement('canvas');
- const ctx = exportCanvas.getContext('2d');
-
- const padding = 32;
- const titleHeight = 56;
- exportCanvas.width = canvas.width + (padding * 2);
- exportCanvas.height = canvas.height + (padding * 2) + titleHeight;
-
- // White background
- ctx.fillStyle = '#ffffff';
- ctx.fillRect(0, 0, exportCanvas.width, exportCanvas.height);
-
- // Draw title
- ctx.fillStyle = '#1e293b';
- ctx.font = '600 24px -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif';
- ctx.textAlign = 'left';
- ctx.fillText(title, padding, padding + 28);
-
- // Draw chart
- ctx.drawImage(canvas, padding, padding + titleHeight);
-
- // Convert to blob and add to zip
- const dataUrl = exportCanvas.toDataURL('image/png', 1.0);
- const base64Data = dataUrl.split(',')[1];
- const safeFilename = title.replace(/[^a-zA-Z0-9\u0600-\u06FF\s-]/g, '').replace(/\s+/g, '-');
- zip.file(`${String(i + 1).padStart(2, '0')}-${safeFilename}.png`, base64Data, { base64: true });
- }
-
- // Generate and download ZIP
- const blob = await zip.generateAsync({ type: 'blob' });
- const url = URL.createObjectURL(blob);
- const link = document.createElement('a');
- link.href = url;
- link.download = `${zipFilename}-${new Date().toISOString().split('T')[0]}.zip`;
- link.click();
- URL.revokeObjectURL(url);
-}
-
-// Button component for exporting all charts
-export function ExportAllButton({ containerSelector, zipFilename, label, loadingLabel }) {
- const [exporting, setExporting] = React.useState(false);
-
- const handleExport = async () => {
- setExporting(true);
- try {
- await exportAllCharts(containerSelector, zipFilename);
- } finally {
- setExporting(false);
- }
- };
-
- return (
-
- );
-}
-
export default ExportableChart;
diff --git a/src/components/Comparison.js b/src/components/Comparison.js
index eea6a24..0018fba 100644
--- a/src/components/Comparison.js
+++ b/src/components/Comparison.js
@@ -4,7 +4,6 @@ import { Line, Bar } from 'react-chartjs-2';
import { EmptyState, FilterControls } from './shared';
import { ExportableChart } from './ChartExport';
import { chartColors, createBaseOptions } from '../config/chartConfig';
-import { useLanguage } from '../contexts/LanguageContext';
import {
filterDataByDateRange,
calculateMetrics,
@@ -40,8 +39,7 @@ const generatePresetDates = (year) => ({
'full': { start: `${year}-01-01`, end: `${year}-12-31` }
});
-function Comparison({ data, showDataLabels, setShowDataLabels }) {
- const { t } = useLanguage();
+function Comparison({ data, showDataLabels }) {
const [searchParams, setSearchParams] = useSearchParams();
// Get available years from data
@@ -142,8 +140,8 @@ function Comparison({ data, showDataLabels, setShowDataLabels }) {
};
const charts = [
- { id: 'timeseries', label: t('comparison.trend') },
- { id: 'museum', label: t('comparison.byMuseum') }
+ { id: 'timeseries', label: 'Trend' },
+ { id: 'museum', label: 'By Museum' }
];
// Touch swipe handlers
@@ -167,16 +165,15 @@ function Comparison({ data, showDataLabels, setShowDataLabels }) {
};
const granularityOptions = [
- { value: 'day', label: t('time.daily') },
- { value: 'week', label: t('time.weekly') },
- { value: 'month', label: t('time.monthly') }
+ { value: 'day', label: 'Daily' },
+ { value: 'week', label: 'Weekly' }
];
const metricOptions = [
- { value: 'revenue', label: t('metrics.revenue'), field: 'revenue_incl_tax', format: 'currency' },
- { value: 'visitors', label: t('metrics.visitors'), field: 'visits', format: 'number' },
- { value: 'tickets', label: t('metrics.tickets'), field: 'tickets', format: 'number' },
- { value: 'avgRevenue', label: t('metrics.avgRevenue'), field: null, format: 'currency' }
+ { value: 'revenue', label: 'Revenue', field: 'revenue_incl_tax', format: 'currency' },
+ { value: 'visitors', label: 'Visitors', field: 'visits', format: 'number' },
+ { value: 'tickets', label: 'Tickets', field: 'tickets', format: 'number' },
+ { value: 'avgRevenue', label: 'Avg Rev/Visitor', field: null, format: 'currency' }
];
const getMetricValue = useCallback((rows, metric) => {
@@ -274,19 +271,19 @@ function Comparison({ data, showDataLabels, setShowDataLabels }) {
const captureRateChange = captureRates ? calcChange(captureRates.prev || 0, captureRates.curr || 0) : null;
const cards = [
- { title: t('metrics.revenue'), prev: prevMetrics.revenue, curr: currMetrics.revenue, change: revenueChange, isCurrency: true },
- { title: t('metrics.visitors'), prev: prevMetrics.visitors, curr: currMetrics.visitors, change: visitorsChange },
- { title: t('metrics.tickets'), prev: prevMetrics.tickets, curr: currMetrics.tickets, change: ticketsChange },
- { title: t('metrics.avgRevenue'), prev: prevMetrics.avgRevPerVisitor, curr: currMetrics.avgRevPerVisitor, change: avgRevChange, isCurrency: true }
+ { title: 'Revenue', prev: prevMetrics.revenue, curr: currMetrics.revenue, change: revenueChange, isCurrency: true },
+ { title: 'Visitors', prev: prevMetrics.visitors, curr: currMetrics.visitors, change: visitorsChange },
+ { title: 'Tickets', prev: prevMetrics.tickets, curr: currMetrics.tickets, change: ticketsChange },
+ { title: 'Avg Rev/Visitor', prev: prevMetrics.avgRevPerVisitor, curr: currMetrics.avgRevPerVisitor, change: avgRevChange, isCurrency: true }
];
if (pilgrimCounts) {
- cards.push({ title: t('metrics.pilgrims'), prev: pilgrimCounts.prev, curr: pilgrimCounts.curr, change: pilgrimsChange, pendingMessage: t('comparison.pendingData') });
+ cards.push({ title: 'Pilgrims', prev: pilgrimCounts.prev, curr: pilgrimCounts.curr, change: pilgrimsChange, pendingMessage: 'Data not published yet' });
}
if (captureRates) {
- cards.push({ title: t('metrics.captureRate'), prev: captureRates.prev, curr: captureRates.curr, change: captureRateChange, isPercent: true, pendingMessage: t('comparison.pendingData') });
+ cards.push({ title: 'Capture Rate', prev: captureRates.prev, curr: captureRates.curr, change: captureRateChange, isPercent: true, pendingMessage: 'Data not published yet' });
}
return cards;
- }, [prevMetrics, currMetrics, pilgrimCounts, captureRates, t]);
+ }, [prevMetrics, currMetrics, pilgrimCounts, captureRates]);
const handleCardTouchStart = (e) => {
touchStartCard.current = e.touches[0].clientX;
@@ -341,11 +338,7 @@ function Comparison({ data, showDataLabels, setShowDataLabels }) {
const daysDiff = Math.floor((rowDate - start) / (1000 * 60 * 60 * 24));
let key;
- if (granularity === 'month') {
- // Group by month number (relative to start)
- const monthsDiff = (rowDate.getFullYear() - start.getFullYear()) * 12 + (rowDate.getMonth() - start.getMonth());
- key = monthsDiff + 1;
- } else if (granularity === 'week') {
+ if (granularity === 'week') {
key = Math.floor(daysDiff / 7) + 1;
} else {
key = daysDiff + 1; // day number from start
@@ -366,15 +359,9 @@ function Comparison({ data, showDataLabels, setShowDataLabels }) {
const currGrouped = groupByPeriod(currData, ranges.curr.start, chartMetric, chartGranularity);
const maxKey = Math.max(...Object.keys(prevGrouped).map(Number), ...Object.keys(currGrouped).map(Number), 1);
- const labels = Array.from({ length: maxKey }, (_, i) => {
- if (chartGranularity === 'month') {
- const startDate = new Date(ranges.curr.start);
- const monthNum = ((startDate.getMonth() + i) % 12) + 1;
- return String(monthNum);
- }
- if (chartGranularity === 'week') return `W${i + 1}`;
- return `D${i + 1}`;
- });
+ const labels = Array.from({ length: maxKey }, (_, i) =>
+ chartGranularity === 'week' ? `W${i + 1}` : `D${i + 1}`
+ );
const prevLabel = getPeriodLabel(ranges.prev.start, ranges.prev.end);
const currLabel = getPeriodLabel(ranges.curr.start, ranges.curr.end);
@@ -436,54 +423,45 @@ function Comparison({ data, showDataLabels, setShowDataLabels }) {
...baseOptions,
plugins: {
...baseOptions.plugins,
- legend: { position: 'top', align: 'end', labels: { boxWidth: 12, padding: 12, font: { size: 13 } } }
+ legend: { position: 'top', align: 'end', labels: { boxWidth: 12, padding: 12, font: { size: 11 } } }
}
};
return (
-
-
-
-
{t('comparison.title')}
-
{t('comparison.subtitle')}
-
-
-
{t('nav.labels')}
-
-
-
-
-
+
+
+
Period Comparison
+
Select a period and year — automatically compares with the same period in the previous year
-
+
-
+
{preset !== 'custom' && (
-
+
+
+
+
{getPeriodLabel(ranges.prev.start, ranges.prev.end)}
+
{formatDate(ranges.prev.start)} → {formatDate(ranges.prev.end)}
+
+
+
{getPeriodLabel(ranges.curr.start, ranges.curr.end)}
+
{formatDate(ranges.curr.start)} → {formatDate(ranges.curr.end)}
+
+
-
-
-
{t('comparison.previousPeriod')}
-
{getPeriodLabel(ranges.prev.start, ranges.prev.end)}
-
{formatDate(ranges.prev.start)} → {formatDate(ranges.prev.end)}
-
-
{t('comparison.vs')}
-
-
{t('comparison.currentPeriod')}
-
{getPeriodLabel(ranges.curr.start, ranges.curr.end)}
-
{formatDate(ranges.curr.start)} → {formatDate(ranges.curr.end)}
-
-
-
{!hasData ? (
) : (
<>
{/* Desktop: Grid layout */}
-
+
{metricCards.map((card, i) => (
{/* Desktop: Show both charts */}
-
+
-
m.value === chartMetric)?.label} - ${t('comparison.trend')}`}
- className="chart-container"
- controls={
- <>
-
- {granularityOptions.map(opt => (
-
- ))}
-
-
- {metricOptions.map(opt => (
-
- ))}
-
- >
- }
- >
-
-
-
-
-
m.value === chartMetric)?.label} - ${t('comparison.byMuseum')}`}
- className="chart-container"
- controls={
+
+
{metricOptions.find(m => m.value === chartMetric)?.label} Trend
+
+
+ {granularityOptions.map(opt => (
+
+ ))}
+
{metricOptions.map(opt => (
- }
- >
+
+
+
+
+
+
+
+
+
{metricOptions.find(m => m.value === chartMetric)?.label} by Museum
+
+
+ {metricOptions.map(opt => (
+
+ ))}
+
+
+
+
@@ -674,7 +644,7 @@ function Comparison({ data, showDataLabels, setShowDataLabels }) {
-
{metricOptions.find(m => m.value === chartMetric)?.label} - {t('comparison.trend')}
+
{metricOptions.find(m => m.value === chartMetric)?.label} Trend
{granularityOptions.map(opt => (