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) {
|
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>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -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 */}
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
Reference in New Issue
Block a user