feat(report): per-museum trend lines in PDF report chart
Deploy HiHala Dashboard / deploy (push) Successful in 11s
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:
@@ -156,7 +156,7 @@ interface Props { data: ReportData; }
|
||||
|
||||
export function ReportDocument({ data }: Props) {
|
||||
const { config: cfg, metrics, prevMetrics, comparisonPeriodLabel,
|
||||
trendLabels, trendCurrent, trendPrevious,
|
||||
trendLabels, trendCurrent, trendPrevious, trendMuseums,
|
||||
museumData, channelBreakdown, districtBreakdown,
|
||||
pilgrimCapture, generatedAt } = data;
|
||||
|
||||
@@ -314,21 +314,33 @@ export function ReportDocument({ data }: Props) {
|
||||
{cfg.showTrendChart && (
|
||||
<View style={S.sectionGap}>
|
||||
<SectionHeading title={trendTitle} color={color} />
|
||||
{cfg.includeComparison && (
|
||||
<View style={S.legendRow}>
|
||||
<View style={S.legendItem}>
|
||||
<View style={[S.legendDot, { backgroundColor: color }]} />
|
||||
<Text style={S.legendLabel}>{period}</Text>
|
||||
<View style={S.legendRow}>
|
||||
{trendMuseums.length >= 2 && trendMuseums.map((m, i) => (
|
||||
<View key={m.name} style={S.legendItem}>
|
||||
<View style={[S.legendDot, { backgroundColor: CHART_PALETTE[i % CHART_PALETTE.length] }]} />
|
||||
<Text style={S.legendLabel}>{m.name}</Text>
|
||||
</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.legendDot, { backgroundColor: '#94a3b8' }]} />
|
||||
<Text style={S.legendLabel}>{comparisonPeriodLabel}</Text>
|
||||
</View>
|
||||
</View>
|
||||
)}
|
||||
)}
|
||||
</View>
|
||||
<View style={S.chartWrap}>
|
||||
<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>
|
||||
)}
|
||||
|
||||
@@ -19,12 +19,14 @@ interface TrendChartProps {
|
||||
current: number[];
|
||||
previous: number[] | null;
|
||||
color: string;
|
||||
series?: Array<{ label: string; color: string; data: number[] }>;
|
||||
width?: number;
|
||||
height?: number;
|
||||
}
|
||||
|
||||
export function PdfTrendChart({ labels, current, previous, color, width = 470, height = 155 }: TrendChartProps) {
|
||||
const allValues = [...current, ...(previous ?? [])].filter(v => v > 0);
|
||||
export function PdfTrendChart({ labels, current, previous, color, series, width = 470, height = 155 }: TrendChartProps) {
|
||||
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;
|
||||
// padL wide enough for y-axis labels like "1.2M"
|
||||
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" />
|
||||
)}
|
||||
|
||||
{/* 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) && (
|
||||
<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 */}
|
||||
|
||||
@@ -106,6 +106,7 @@ export interface ReportData {
|
||||
trendLabels: string[];
|
||||
trendCurrent: number[];
|
||||
trendPrevious: number[] | null;
|
||||
trendMuseums: Array<{ name: string; values: number[] }>;
|
||||
museumData: MuseumDataRow[];
|
||||
museumBreakdown: DimensionBreakdown;
|
||||
channelBreakdown: DimensionBreakdown;
|
||||
@@ -195,6 +196,11 @@ export function computeReportData(allData: MuseumRecord[], cfg: ReportConfig): R
|
||||
? Array.from({ length: maxLen }, (_, i) => prevTrend.values[i] ?? 0)
|
||||
: 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 prevMuseumGroups = cfg.includeComparison ? groupByMuseum(prevRows, cfg.includeVAT) : {};
|
||||
const museumData: MuseumDataRow[] = Object.entries(currMuseumGroups)
|
||||
@@ -226,6 +232,7 @@ export function computeReportData(allData: MuseumRecord[], cfg: ReportConfig): R
|
||||
trendLabels,
|
||||
trendCurrent,
|
||||
trendPrevious,
|
||||
trendMuseums,
|
||||
museumData,
|
||||
museumBreakdown,
|
||||
channelBreakdown,
|
||||
|
||||
Reference in New Issue
Block a user