feat(report): per-museum trend lines in PDF report chart
Deploy HiHala Dashboard / deploy (push) Successful in 11s

When multiple museums are present, the report trend chart now renders one
colored line per museum plus a bold Total line, mirroring dashboard behavior.
Legend is updated to list each museum with its corresponding color.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
fahed
2026-04-30 10:56:26 +03:00
parent 7365bc808b
commit 131868a280
3 changed files with 40 additions and 13 deletions
+21 -9
View File
@@ -156,7 +156,7 @@ interface Props { data: ReportData; }
export function ReportDocument({ data }: Props) { export function ReportDocument({ data }: Props) {
const { config: cfg, metrics, prevMetrics, comparisonPeriodLabel, const { config: cfg, metrics, prevMetrics, comparisonPeriodLabel,
trendLabels, trendCurrent, trendPrevious, trendLabels, trendCurrent, trendPrevious, trendMuseums,
museumData, channelBreakdown, districtBreakdown, museumData, channelBreakdown, districtBreakdown,
pilgrimCapture, generatedAt } = data; pilgrimCapture, generatedAt } = data;
@@ -314,21 +314,33 @@ export function ReportDocument({ data }: Props) {
{cfg.showTrendChart && ( {cfg.showTrendChart && (
<View style={S.sectionGap}> <View style={S.sectionGap}>
<SectionHeading title={trendTitle} color={color} /> <SectionHeading title={trendTitle} color={color} />
{cfg.includeComparison && ( <View style={S.legendRow}>
<View style={S.legendRow}> {trendMuseums.length >= 2 && trendMuseums.map((m, i) => (
<View style={S.legendItem}> <View key={m.name} style={S.legendItem}>
<View style={[S.legendDot, { backgroundColor: color }]} /> <View style={[S.legendDot, { backgroundColor: CHART_PALETTE[i % CHART_PALETTE.length] }]} />
<Text style={S.legendLabel}>{period}</Text> <Text style={S.legendLabel}>{m.name}</Text>
</View> </View>
))}
<View style={S.legendItem}>
<View style={[S.legendDot, { backgroundColor: color }]} />
<Text style={S.legendLabel}>{trendMuseums.length >= 2 ? `Total · ${period}` : period}</Text>
</View>
{cfg.includeComparison && (
<View style={S.legendItem}> <View style={S.legendItem}>
<View style={[S.legendDot, { backgroundColor: '#94a3b8' }]} /> <View style={[S.legendDot, { backgroundColor: '#94a3b8' }]} />
<Text style={S.legendLabel}>{comparisonPeriodLabel}</Text> <Text style={S.legendLabel}>{comparisonPeriodLabel}</Text>
</View> </View>
</View> )}
)} </View>
<View style={S.chartWrap}> <View style={S.chartWrap}>
<PdfTrendChart labels={trendLabels} current={trendCurrent} <PdfTrendChart labels={trendLabels} current={trendCurrent}
previous={trendPrevious} color={color} width={chartW} height={155} /> previous={trendPrevious} color={color} width={chartW} height={155}
series={trendMuseums.length >= 2 ? trendMuseums.map((m, i) => ({
label: m.name,
color: CHART_PALETTE[i % CHART_PALETTE.length],
data: m.values,
})) : undefined}
/>
</View> </View>
</View> </View>
)} )}
+12 -4
View File
@@ -19,12 +19,14 @@ interface TrendChartProps {
current: number[]; current: number[];
previous: number[] | null; previous: number[] | null;
color: string; color: string;
series?: Array<{ label: string; color: string; data: number[] }>;
width?: number; width?: number;
height?: number; height?: number;
} }
export function PdfTrendChart({ labels, current, previous, color, width = 470, height = 155 }: TrendChartProps) { export function PdfTrendChart({ labels, current, previous, color, series, width = 470, height = 155 }: TrendChartProps) {
const allValues = [...current, ...(previous ?? [])].filter(v => v > 0); const seriesValues = (series ?? []).flatMap(s => s.data);
const allValues = [...current, ...(previous ?? []), ...seriesValues].filter(v => v > 0);
const max = allValues.length > 0 ? Math.max(...allValues) : 1; const max = allValues.length > 0 ? Math.max(...allValues) : 1;
// padL wide enough for y-axis labels like "1.2M" // padL wide enough for y-axis labels like "1.2M"
const padL = 38, padR = 8, padT = 10, padB = 20; const padL = 38, padR = 8, padT = 10, padB = 20;
@@ -66,10 +68,16 @@ export function PdfTrendChart({ labels, current, previous, color, width = 470, h
stroke="#94a3b8" strokeWidth={1.5} strokeDasharray="4 3" fill="none" /> stroke="#94a3b8" strokeWidth={1.5} strokeDasharray="4 3" fill="none" />
)} )}
{/* Current period line */} {/* Per-museum series */}
{(series ?? []).map(s => s.data.some(v => v > 0) && (
<Polyline key={s.label} points={toPoints(s.data)}
stroke={s.color} strokeWidth={1.5} fill="none" />
))}
{/* Current period total line */}
{current.some(v => v > 0) && ( {current.some(v => v > 0) && (
<Polyline points={toPoints(current)} <Polyline points={toPoints(current)}
stroke={color} strokeWidth={2.5} fill="none" /> stroke={color} strokeWidth={series && series.length >= 2 ? 2 : 2.5} fill="none" />
)} )}
{/* X-axis week labels */} {/* X-axis week labels */}
+7
View File
@@ -106,6 +106,7 @@ export interface ReportData {
trendLabels: string[]; trendLabels: string[];
trendCurrent: number[]; trendCurrent: number[];
trendPrevious: number[] | null; trendPrevious: number[] | null;
trendMuseums: Array<{ name: string; values: number[] }>;
museumData: MuseumDataRow[]; museumData: MuseumDataRow[];
museumBreakdown: DimensionBreakdown; museumBreakdown: DimensionBreakdown;
channelBreakdown: DimensionBreakdown; channelBreakdown: DimensionBreakdown;
@@ -195,6 +196,11 @@ export function computeReportData(allData: MuseumRecord[], cfg: ReportConfig): R
? Array.from({ length: maxLen }, (_, i) => prevTrend.values[i] ?? 0) ? Array.from({ length: maxLen }, (_, i) => prevTrend.values[i] ?? 0)
: null; : null;
const trendMuseums = Object.keys(groupByMuseum(currRows, cfg.includeVAT)).map(name => {
const mt = buildTrend(currRows.filter(r => r.museum_name === name), cfg.startDate, cfg);
return { name, values: Array.from({ length: maxLen }, (_, i) => mt.values[i] ?? 0) };
});
const currMuseumGroups = groupByMuseum(currRows, cfg.includeVAT); const currMuseumGroups = groupByMuseum(currRows, cfg.includeVAT);
const prevMuseumGroups = cfg.includeComparison ? groupByMuseum(prevRows, cfg.includeVAT) : {}; const prevMuseumGroups = cfg.includeComparison ? groupByMuseum(prevRows, cfg.includeVAT) : {};
const museumData: MuseumDataRow[] = Object.entries(currMuseumGroups) const museumData: MuseumDataRow[] = Object.entries(currMuseumGroups)
@@ -226,6 +232,7 @@ export function computeReportData(allData: MuseumRecord[], cfg: ReportConfig): R
trendLabels, trendLabels,
trendCurrent, trendCurrent,
trendPrevious, trendPrevious,
trendMuseums,
museumData, museumData,
museumBreakdown, museumBreakdown,
channelBreakdown, channelBreakdown,