Data_Analysis/analytics-demo-web/src/config/analysis-config.ts

1384 lines
53 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import type { EChartsOption } from 'echarts'
import type { AnalysisContext, AnalysisPageDefinition, ChartDefinition } from '../types/analysis'
import {
buildAeMonthlyByOccurrence,
buildComplaintPareto,
buildComplianceMatching,
buildGlobalMonthlyPpm,
buildInjuryDeviceMatrix,
buildProvinceAeCounts,
buildPsurTopProducts,
buildReportingLagDays,
buildSaeMonthlyByOccurrence,
buildSaeShareMonthly,
buildTopDeviceFailures,
} from '../lib/aggregate'
import {
buildBatchComplaintVsAeCounts,
buildComplaintBatchTop,
buildComplaintCloseCycleDays,
buildComplaintMonthly,
buildCompensationDist,
buildDefectConfirmedProductFault,
buildFaultByRegisterMonthMatrix,
buildHospitalAeVsAdmissionScatter,
buildHospitalComplaintVsAdmissionQty,
buildHospitalProductComplaintRate,
buildInvestigationConclusionDist,
buildIsAeRateByProduct,
buildProductComplaintHalfYearGrowth,
buildProvinceComplaintHalfYearGrowth,
buildTopFaultTypes,
buildTopHospitalsComplaints,
buildTopProductsComplaints,
} from '../lib/quality-aggregate'
import {
COMPLIANCE_DOC_REF,
COMPLIANCE_FOOTNOTE_AUDIT,
COMPLIANCE_FOOTNOTE_PPM,
COMPLIANCE_FOOTNOTE_SHORT,
} from './compliance-baseline'
import { QUALITY_DOC_REF, QLT_ADMISSION_DENOM, QLT_TIME_AXIS } from './quality-baseline'
import {
buildDealerComplaintRatePer1000,
buildDealerOpWrongRatePer1000,
buildOpWrongDealerCounts,
buildOpWrongHospitalTop,
buildProductOpWrongSharePct,
buildProvinceComplaintCounts,
buildProvinceComplaintRatePer1000,
} from '../lib/marketing-aggregate'
import { MARKETING_DOC_REF, MKT_ADMISSION_DENOM, MKT_HOSPITAL_MAP, MKT_TIME_AXIS } from './marketing-baseline'
const MOCK_DATA_SCOPE =
'数据源:前端 fetch 加载 public/mock 下 ae.json、complaint.json、admission.json 的全量记录;未接入 Pinia 全局年份/BU 筛选(上线后与数仓条件对齐)。'
const ex = (extraScope: string, analysisLogic: string) => ({
dataScope: [MOCK_DATA_SCOPE, extraScope].filter(Boolean).join(' '),
analysisLogic,
})
const placeholderPair = (prefix: string, hint = '见主策略对应小节与落地文档指标编码。'): ChartDefinition[] => [
{
id: `${prefix}-1`,
title: '图表位 A待接数据',
placeholder: true,
chartExplain: ex('暂无聚合结果。', `占位图表;实现时替换为 optionBuilder并满足「取数范围 + 分析逻辑」说明要求。${hint}`),
},
{
id: `${prefix}-2`,
title: '图表位 B待接数据',
placeholder: true,
chartExplain: ex('暂无聚合结果。', `同上。${hint}`),
},
]
const lineOption = (
title: string,
xAxis: string[],
seriesData: (number | null)[],
seriesName = '事件数',
subtext?: string,
): EChartsOption => ({
title: {
text: title,
subtext,
left: 'center',
textStyle: { fontSize: 14 },
subtextStyle: { fontSize: 11, color: '#64748b' },
},
tooltip: { trigger: 'axis' },
grid: { top: subtext ? 78 : 60, right: 24, bottom: 40, left: 40 },
xAxis: { type: 'category', data: xAxis },
yAxis: { type: 'value' },
series: [{ name: seriesName, type: 'line', smooth: true, data: seriesData }],
})
const barOption = (title: string, labels: string[], values: number[], subtext?: string): EChartsOption => ({
title: {
text: title,
subtext,
left: 'center',
textStyle: { fontSize: 14 },
subtextStyle: { fontSize: 11, color: '#64748b' },
},
tooltip: { trigger: 'axis' },
grid: { top: subtext ? 78 : 60, right: 24, bottom: 60, left: 45 },
xAxis: { type: 'category', data: labels, axisLabel: { rotate: 20 } },
yAxis: { type: 'value' },
series: [{ type: 'bar', data: values, itemStyle: { borderRadius: [4, 4, 0, 0] } }],
})
const injuryDeviceHeatmapOption = (saeOnly: boolean, title: string, subtext: string) => {
return (context: AnalysisContext): EChartsOption => {
const { injuries, devices, cells } = buildInjuryDeviceMatrix(context, saeOnly)
const data = cells.map((c) => [devices.indexOf(c.device), injuries.indexOf(c.injury), c.value])
const max = Math.max(1, ...cells.map((c) => c.value))
return {
title: {
text: title,
subtext,
left: 'center',
textStyle: { fontSize: 14 },
subtextStyle: { fontSize: 11, color: '#64748b' },
},
tooltip: { position: 'top' },
grid: { top: 88, height: Math.max(220, injuries.length * 28), left: 80, right: 24, bottom: 48 },
xAxis: { type: 'category', data: devices, splitArea: { show: true } },
yAxis: { type: 'category', data: injuries, splitArea: { show: true } },
visualMap: {
min: 0,
max,
calculable: true,
orient: 'horizontal',
left: 'center',
bottom: 4,
inRange: { color: ['#eef2ff', '#312e81'] },
},
series: [
{
name: '报告条数',
type: 'heatmap',
data,
label: { show: true },
emphasis: { itemStyle: { shadowBlur: 10, shadowColor: 'rgba(0,0,0,0.3)' } },
},
],
}
}
}
const reportingLagHistogramOption = (context: AnalysisContext): EChartsOption => {
const lags = buildReportingLagDays(context).filter((d) => d >= 0)
const bins = ['01', '23', '45', '67', '810', '>10']
const counts = [0, 0, 0, 0, 0, 0]
lags.forEach((d) => {
if (d <= 1) counts[0] += 1
else if (d <= 3) counts[1] += 1
else if (d <= 5) counts[2] += 1
else if (d <= 7) counts[3] += 1
else if (d <= 10) counts[4] += 1
else counts[5] += 1
})
return {
title: {
text: '上报滞后天数分布(审核日期 登记日期)',
subtext: `${COMPLIANCE_DOC_REF}CMP-AE-701`,
left: 'center',
textStyle: { fontSize: 14 },
subtextStyle: { fontSize: 11, color: '#64748b' },
},
tooltip: { trigger: 'axis' },
grid: { top: 88, right: 24, bottom: 40, left: 40 },
xAxis: { type: 'category', data: bins },
yAxis: { type: 'value', name: 'AE 条数' },
series: [{ type: 'bar', data: counts, itemStyle: { borderRadius: [4, 4, 0, 0] } }],
}
}
const complianceAuditSummaryOption = (context: AnalysisContext): EChartsOption => {
const m = buildComplianceMatching(context)
return {
title: {
text: '上报合规审计摘要',
subtext: COMPLIANCE_FOOTNOTE_AUDIT,
left: 'center',
textStyle: { fontSize: 14 },
subtextStyle: { fontSize: 11, color: '#64748b' },
},
tooltip: { trigger: 'axis' },
legend: { top: 72, data: ['占比%'] },
grid: { top: 110, right: 24, bottom: 56, left: 48 },
xAxis: {
type: 'category',
data: ['漏报率', '匹配率', '时效(登记)', '时效(C3)'],
axisLabel: { interval: 0, rotate: 0 },
},
yAxis: { type: 'value', name: '%', max: 100 },
series: [
{
name: '占比%',
type: 'bar',
data: [m.leakRate, m.matchRate, m.timelinessFromRegistrationPct, m.timelinessFromC3Pct],
itemStyle: { borderRadius: [4, 4, 0, 0] },
},
],
}
}
const complianceAuditCountsOption = (context: AnalysisContext): EChartsOption => {
const m = buildComplianceMatching(context)
return {
title: {
text: '应报样本量(投诉 isAe=是)',
subtext: `基准 ${m.flaggedTotal} 条|匹配 ${m.matched}|疑似漏报 ${m.missed}CMP-CV-101~104`,
left: 'center',
textStyle: { fontSize: 14 },
subtextStyle: { fontSize: 11, color: '#64748b' },
},
tooltip: { trigger: 'item' },
series: [
{
type: 'pie',
radius: ['36%', '62%'],
label: { formatter: '{b}: {c} ({d}%)' },
data: [
{ name: '已匹配', value: m.matched },
{ name: '疑似漏报', value: m.missed },
],
},
],
}
}
const paretoOption = (context: AnalysisContext): EChartsOption => {
const pareto = buildComplaintPareto(context)
return {
title: { text: '产品×故障 Pareto', left: 'center', textStyle: { fontSize: 14 } },
tooltip: { trigger: 'axis' },
legend: { top: 30, data: ['投诉数', '累计占比%'] },
grid: { top: 70, right: 50, bottom: 70, left: 45 },
xAxis: { type: 'category', data: pareto.labels, axisLabel: { rotate: 25 } },
yAxis: [{ type: 'value', name: '投诉数' }, { type: 'value', name: '累计%', max: 100 }],
series: [
{ name: '投诉数', type: 'bar', data: pareto.bars },
{ name: '累计占比%', type: 'line', yAxisIndex: 1, smooth: true, data: pareto.line },
],
}
}
const faultSeasonalityHeatmapOption = (context: AnalysisContext): EChartsOption => {
const { faults, months, cells } = buildFaultByRegisterMonthMatrix(context)
const max = Math.max(1, ...cells.map((c) => c.value))
const data = cells.map((c) => [months.indexOf(c.month), faults.indexOf(c.fault), c.value])
return {
title: {
text: '故障类型 × C3登记月投诉热力',
subtext: `${QUALITY_DOC_REF}|主策略 §5.2 与 QLT-BTH-203 月份桶一致`,
left: 'center',
textStyle: { fontSize: 14 },
subtextStyle: { fontSize: 11, color: '#64748b' },
},
tooltip: { position: 'top' },
grid: { top: 88, height: Math.max(220, faults.length * 26), left: 100, right: 24, bottom: 48 },
xAxis: { type: 'category', data: months, splitArea: { show: true } },
yAxis: { type: 'category', data: faults, splitArea: { show: true } },
visualMap: {
min: 0,
max,
calculable: true,
orient: 'horizontal',
left: 'center',
bottom: 4,
inRange: { color: ['#ecfdf5', '#065f46'] },
},
series: [{ name: '投诉条数', type: 'heatmap', data, label: { show: true } }],
}
}
const closeCycleHistogramOption = (context: AnalysisContext): EChartsOption => {
const days = buildComplaintCloseCycleDays(context)
const bins = ['07', '814', '1530', '3160', '>60']
const counts = [0, 0, 0, 0, 0]
days.forEach((d) => {
if (d <= 7) counts[0] += 1
else if (d <= 14) counts[1] += 1
else if (d <= 30) counts[2] += 1
else if (d <= 60) counts[3] += 1
else counts[4] += 1
})
return {
title: {
text: '投诉关闭周期分布',
subtext: 'QLT-RPT-802关闭日期 C3登记日期',
left: 'center',
textStyle: { fontSize: 14 },
subtextStyle: { fontSize: 11, color: '#64748b' },
},
tooltip: { trigger: 'axis' },
grid: { top: 80, right: 24, bottom: 40, left: 44 },
xAxis: { type: 'category', data: bins },
yAxis: { type: 'value', name: '投诉条数' },
series: [{ type: 'bar', data: counts, itemStyle: { borderRadius: [4, 4, 0, 0] } }],
}
}
const hospitalAeScatterOption = (context: AnalysisContext): EChartsOption => {
const pts = buildHospitalAeVsAdmissionScatter(context)
const data = pts.map((p) => [p.cyQty, p.aeCount, p.hospital])
return {
title: {
text: '医院AE 条数 vs 入院 cyQty 汇总',
subtext: 'QLT-AE-1003AE 按单位名称计数;入院量按医院汇总',
left: 'center',
textStyle: { fontSize: 14 },
subtextStyle: { fontSize: 11, color: '#64748b' },
},
tooltip: {
formatter: (params: unknown) => {
const p = params as { value?: [number, number, string] }
const v = p.value
if (!v) return ''
return `${v[2]}<br/>cyQty${v[0]}<br/>AE${v[1]}`
},
},
grid: { top: 88, right: 24, bottom: 44, left: 52 },
xAxis: { type: 'value', name: 'cyQty', scale: true },
yAxis: { type: 'value', name: 'AE 条数', scale: true },
series: [{ type: 'scatter', symbolSize: 12, data }],
}
}
const hospitalComplaintQtyScatterOption = (context: AnalysisContext): EChartsOption => {
const pts = buildHospitalComplaintVsAdmissionQty(context)
const data = pts.map((p) => [p.cyQty, p.complaints, p.hospital])
return {
title: {
text: '医院:投诉条数 vs 入院 cyQty',
subtext: QLT_TIME_AXIS,
left: 'center',
textStyle: { fontSize: 14 },
subtextStyle: { fontSize: 11, color: '#64748b' },
},
tooltip: {
formatter: (params: unknown) => {
const p = params as { value?: [number, number, string] }
const v = p.value
if (!v) return ''
return `${v[2]}<br/>cyQty${v[0]}<br/>投诉:${v[1]}`
},
},
grid: { top: 88, right: 24, bottom: 44, left: 52 },
xAxis: { type: 'value', name: 'cyQty', scale: true },
yAxis: { type: 'value', name: '投诉条数', scale: true },
series: [{ type: 'scatter', symbolSize: 12, data }],
}
}
const productHalfYearBarOption = (context: AnalysisContext): EChartsOption => {
const h1 = new Map<string, number>()
const h2 = new Map<string, number>()
for (const c of context.complaints) {
const mo = Number.parseInt(c.registerDate.slice(5, 7), 10)
const p = c.productName
if (mo <= 6) h1.set(p, (h1.get(p) ?? 0) + 1)
else h2.set(p, (h2.get(p) ?? 0) + 1)
}
const products = [...new Set([...h1.keys(), ...h2.keys()])].sort()
return {
title: {
text: '产品投诉量:上半年 vs 下半年',
subtext: 'C3登记月份 16 vs 712QLT-TRN-703 对比窗演示',
left: 'center',
textStyle: { fontSize: 14 },
subtextStyle: { fontSize: 11, color: '#64748b' },
},
tooltip: { trigger: 'axis' },
legend: { top: 72 },
grid: { top: 100, right: 24, bottom: 56, left: 44 },
xAxis: { type: 'category', data: products, axisLabel: { rotate: 22 } },
yAxis: { type: 'value', name: '投诉条数' },
series: [
{ name: '上半年(16月)', type: 'bar', data: products.map((p) => h1.get(p) ?? 0) },
{ name: '下半年(712月)', type: 'bar', data: products.map((p) => h2.get(p) ?? 0) },
],
}
}
const productGrowthAlertBarOption = (context: AnalysisContext): EChartsOption => {
const alertRows = buildProductComplaintHalfYearGrowth(context)
const h1 = new Map<string, number>()
const h2 = new Map<string, number>()
for (const c of context.complaints) {
const mo = Number.parseInt(c.registerDate.slice(5, 7), 10)
if (mo <= 6) h1.set(c.productName, (h1.get(c.productName) ?? 0) + 1)
else h2.set(c.productName, (h2.get(c.productName) ?? 0) + 1)
}
const labels = alertRows.length
? alertRows.map((r) => r.product)
: [...new Set(context.complaints.map((c) => c.productName))].sort()
return {
title: {
text: '产品投诉增速预警(下半年相对上半年 ≥20%',
subtext: alertRows.length ? 'QLT-TRN-703' : 'QLT-TRN-703当前样本无达阈产品展示全产品上下半年对照',
left: 'center',
textStyle: { fontSize: 14 },
subtextStyle: { fontSize: 11, color: '#64748b' },
},
tooltip: { trigger: 'axis' },
legend: { top: 72 },
grid: { top: 100, right: 24, bottom: 56, left: 44 },
xAxis: { type: 'category', data: labels, axisLabel: { rotate: 22 } },
yAxis: { type: 'value', name: '投诉条数' },
series: [
{ name: '上半年', type: 'bar', data: labels.map((p) => h1.get(p) ?? 0) },
{ name: '下半年', type: 'bar', data: labels.map((p) => h2.get(p) ?? 0) },
],
}
}
const provinceHalfYearBarOption = (context: AnalysisContext): EChartsOption => {
const rows = buildProvinceComplaintHalfYearGrowth(context)
const labels = rows.map((r) => r.province)
return {
title: {
text: '省份投诉量:上半年 vs 下半年',
subtext: '医院→省映射取自入院量表众数QLT-TRN-704',
left: 'center',
textStyle: { fontSize: 14 },
subtextStyle: { fontSize: 11, color: '#64748b' },
},
tooltip: { trigger: 'axis' },
legend: { top: 72 },
grid: { top: 100, right: 24, bottom: 48, left: 44 },
xAxis: { type: 'category', data: labels },
yAxis: { type: 'value', name: '投诉条数' },
series: [
{ name: '上半年', type: 'bar', data: rows.map((r) => r.firstHalf) },
{ name: '下半年', type: 'bar', data: rows.map((r) => r.secondHalf) },
],
}
}
const complaintRateBarOption = (context: AnalysisContext): EChartsOption => {
const rows = buildHospitalProductComplaintRate(context).slice(0, 10)
const labels = rows.map((r) => `${r.hospital} / ${r.product}`)
return {
title: {
text: '医院×产品 投诉率 Top每千件入院量',
subtext: `QLT-SAL-901${QLT_ADMISSION_DENOM}`,
left: 'center',
textStyle: { fontSize: 14 },
subtextStyle: { fontSize: 11, color: '#64748b' },
},
tooltip: { trigger: 'axis' },
grid: { top: 88, right: 24, bottom: 8, left: 8, containLabel: true },
xAxis: { type: 'value', name: '件/千件' },
yAxis: { type: 'category', data: labels, axisLabel: { fontSize: 10 } },
series: [{ type: 'bar', data: rows.map((r) => r.ratePer1000), itemStyle: { borderRadius: [0, 4, 4, 0] } }],
}
}
const isAeRateBarOption = (context: AnalysisContext): EChartsOption => {
const rows = buildIsAeRateByProduct(context)
return {
title: {
text: '按产品的投诉升级率(是否不良事件=是)',
subtext: 'QLT-AE-1001',
left: 'center',
textStyle: { fontSize: 14 },
subtextStyle: { fontSize: 11, color: '#64748b' },
},
tooltip: { trigger: 'axis' },
grid: { top: 72, right: 24, bottom: 48, left: 44 },
xAxis: { type: 'category', data: rows.map((r) => r.product), axisLabel: { rotate: 18 } },
yAxis: { type: 'value', name: '%', max: 100 },
series: [{ type: 'bar', data: rows.map((r) => r.ratePct), itemStyle: { borderRadius: [4, 4, 0, 0] } }],
}
}
const investigationPieOption = (context: AnalysisContext): EChartsOption => {
const dist = buildInvestigationConclusionDist(context)
return {
title: {
text: '调查结论分布',
subtext: 'QLT-INV-501',
left: 'center',
textStyle: { fontSize: 14 },
subtextStyle: { fontSize: 11, color: '#64748b' },
},
tooltip: { trigger: 'item' },
series: [
{
type: 'pie',
radius: ['34%', '60%'],
label: { formatter: '{b}: {c} ({d}%)' },
data: dist.map(([name, value]) => ({ name, value })),
},
],
}
}
const compensationPieOption = (context: AnalysisContext): EChartsOption => {
const dist = buildCompensationDist(context)
return {
title: {
text: '赔付结论分布',
subtext: 'QLT-CST-601',
left: 'center',
textStyle: { fontSize: 14 },
subtextStyle: { fontSize: 11, color: '#64748b' },
},
tooltip: { trigger: 'item' },
series: [
{
type: 'pie',
radius: ['34%', '60%'],
label: { formatter: '{b}: {c} ({d}%)' },
data: dist.map(([name, value]) => ({ name, value })),
},
],
}
}
const defectProductFaultBarOption = (context: AnalysisContext): EChartsOption => {
const d = buildDefectConfirmedProductFault(context)
return barOption('产品缺陷成立:产品×故障 Top', d.labels, d.values, 'QLT-INV-502')
}
const batchComplaintAeBarOption = (context: AnalysisContext): EChartsOption => {
const rows = buildBatchComplaintVsAeCounts(context).filter((r) => r.complaints + r.aeReports > 0)
return {
title: {
text: '批号:投诉条数 vs AE 条数',
subtext: 'QLT-BTH-104同批号字符串对照非逐条匹配',
left: 'center',
textStyle: { fontSize: 14 },
subtextStyle: { fontSize: 11, color: '#64748b' },
},
tooltip: { trigger: 'axis' },
legend: { top: 72 },
grid: { top: 100, right: 24, bottom: 48, left: 44 },
xAxis: { type: 'category', data: rows.map((r) => r.batchNo), axisLabel: { rotate: 18 } },
yAxis: { type: 'value', name: '条数' },
series: [
{ name: '投诉', type: 'bar', data: rows.map((r) => r.complaints) },
{ name: 'AE', type: 'bar', data: rows.map((r) => r.aeReports) },
],
}
}
const complaintMonthlyLineOption = (context: AnalysisContext): EChartsOption => {
const t = buildComplaintMonthly(context)
return lineOption('投诉月度趋势C3登记月', t.months, t.values, '投诉条数', `${QUALITY_DOC_REF}QLT 时间轴`)
}
const marketingHBarOption = (
title: string,
subtext: string,
labels: string[],
values: number[],
xAxisName: string,
): EChartsOption => ({
title: {
text: title,
subtext,
left: 'center',
textStyle: { fontSize: 14 },
subtextStyle: { fontSize: 11, color: '#64748b' },
},
tooltip: { trigger: 'axis' },
grid: { top: 88, right: 28, bottom: 12, left: 12, containLabel: true },
xAxis: { type: 'value', name: xAxisName },
yAxis: { type: 'category', data: labels, axisLabel: { fontSize: 10 } },
series: [{ type: 'bar', data: values, itemStyle: { borderRadius: [0, 4, 4, 0] } }],
})
const mktOpWrongHospitalOption = (context: AnalysisContext): EChartsOption => {
const d = buildOpWrongHospitalTop(context, 14)
return barOption('操作不当投诉:医院 Top', d.labels, d.values, 'MKT-OP-101')
}
const mktOpWrongDealerCountOption = (context: AnalysisContext): EChartsOption => {
const d = buildOpWrongDealerCounts(context, 14)
return barOption('操作不当投诉:经销商(条数)', d.labels, d.values, 'MKT-OP-102')
}
const mktDealerOpWrongRateOption = (context: AnalysisContext): EChartsOption => {
const d = buildDealerOpWrongRatePer1000(context, 14)
return marketingHBarOption(
'经销商操作不当率(件/千件入院量)',
`MKT-OP-103${MARKETING_DOC_REF}`,
d.labels,
d.values,
'件/千件',
)
}
const mktProductOpWrongShareOption = (context: AnalysisContext): EChartsOption => {
const d = buildProductOpWrongSharePct(context, 12)
return marketingHBarOption(
'产品:操作不当占该产品投诉比例(%',
'MKT-OP-104子集=调查结论为「操作不当」',
d.labels,
d.values,
'%',
)
}
const mktProvinceComplaintRateOption = (context: AnalysisContext): EChartsOption => {
const d = buildProvinceComplaintRatePer1000(context, 16)
return marketingHBarOption(
'省级投诉率(件/千件入院量)',
`MKT-RG-201${MKT_ADMISSION_DENOM}`,
d.labels,
d.values,
'件/千件',
)
}
const mktDealerComplaintRateOption = (context: AnalysisContext): EChartsOption => {
const d = buildDealerComplaintRatePer1000(context, 16)
return marketingHBarOption(
'经销商投诉率(件/千件入院量)',
`MKT-RG-202${MARKETING_DOC_REF}`,
d.labels,
d.values,
'件/千件',
)
}
const mktProvinceComplaintCountOption = (context: AnalysisContext): EChartsOption => {
const d = buildProvinceComplaintCounts(context, 14)
return barOption('省级投诉条数 Top绝对量', d.labels, d.values, 'MKT-RG-203')
}
const configs: Record<string, AnalysisPageDefinition> = {
'compliance-psur': {
key: 'compliance-psur',
title: 'PSUR 监管分析',
pillar: 'monitor',
strategyRef: '合规方向分析 §2',
subtitle: '监测层按《合规方向分析》CMP-AE-101/201/301/401/501/601/701 等演示实现。',
sections: [
{
id: 'psur-trend',
title: '总体趋势与暴露',
strategyRefs: ['CMP-AE-101', 'CMP-AE-201', 'CMP-AE-301'],
charts: [
{
id: 'psur-monthly',
title: '不良事件月度趋势(按发生日期)',
description: COMPLIANCE_FOOTNOTE_SHORT,
chartExplain: ex(
'不良事件表;时间过滤=发生日期 occurrenceDate统计单元=每条 AE 记录计 1。',
'CMP-AE-101按公历月对报告条数求和用于 PSUR「总体趋势」与异常上升监测。',
),
optionBuilder: (context) => {
const trend = buildAeMonthlyByOccurrence(context)
return lineOption(
'CMP-AE-101 不良事件报告数',
trend.months,
trend.values,
'事件数',
`${COMPLIANCE_DOC_REF}|主时间轴=发生日期|单元=报告条数`,
)
},
},
{
id: 'psur-ppm',
title: '全局月度 PPM入院量 cyQty 分母)',
description: COMPLIANCE_FOOTNOTE_PPM,
chartExplain: ex(
'分子=AE 按发生月计数;分母=入院量表按业务年月汇总 cyQty演示未按产品拆分全量暴露。',
'CMP-AE-201PPM = AE数/入院量×1e6分母为 0 时在序列中用 null 表示不可算。',
),
optionBuilder: (context) => {
const ppm = buildGlobalMonthlyPpm(context)
return lineOption(
'CMP-AE-201 演示 PPM',
ppm.months,
ppm.ppm,
'PPM',
`${COMPLIANCE_DOC_REF}|分母=入院量 cyQty 按年月汇总`,
)
},
},
{
id: 'psur-device-top',
title: '器械故障表现 Top占比',
description: COMPLIANCE_FOOTNOTE_SHORT,
chartExplain: ex(
'不良事件表 deviceFailure 字段;全量计数。',
'CMP-AE-301按故障表现归类计数取 Toptooltip 可扩展占比=该类条数/当期 AE 总数。',
),
optionBuilder: (context) => {
const top = buildTopDeviceFailures(context, 10)
return barOption(
'CMP-AE-301 器械故障 Top',
top.labels,
top.values,
'条数=报告条数;占比见 tooltip',
)
},
},
{
id: 'psur-product-top',
title: '产品事件 Top 分布',
description: COMPLIANCE_FOOTNOTE_SHORT,
chartExplain: ex('不良事件表 productName 全量。', '按产品聚合 AE 报告条数取 Top用于 PSUR 产品章节与管理驾驶舱分产品线视图。'),
optionBuilder: (context) => {
const top = buildPsurTopProducts(context)
return barOption('产品 AE 条数 Top', top.labels, top.values, COMPLIANCE_DOC_REF)
},
},
],
},
{
id: 'psur-serious',
title: '严重事件、区域与上报时效',
strategyRefs: ['CMP-AE-501', 'CMP-AE-502', 'CMP-AE-601', 'CMP-AE-701'],
charts: [
{
id: 'psur-sae-monthly',
title: 'SAE 报告数(按发生月)',
description: 'SAE死亡/危及生命/残疾/住院或延长住院 任一(演示字段)',
chartExplain: ex(
'不良事件表SAE 子集由四个布尔字段任一为真判定(与合规方向分析 §1.4 一致)。',
'CMP-AE-501在 SAE 子集上按发生月计数,用于严重事件专章趋势。',
),
optionBuilder: (context) => {
const s = buildSaeMonthlyByOccurrence(context)
return lineOption('CMP-AE-501 SAE 条数', s.months, s.values, 'SAE 条数', COMPLIANCE_DOC_REF)
},
},
{
id: 'psur-sae-share',
title: 'SAE 占全部 AE 比例(按发生月)',
description: 'CMP-AE-502',
chartExplain: ex('同月内 SAE 条数与全部 AE 条数之比,时间轴=发生月。', 'CMP-AE-502观察严重事件是否在总量上升同时占比上升。'),
optionBuilder: (context) => {
const s = buildSaeShareMonthly(context)
return lineOption('CMP-AE-502 SAE%', s.months, s.shares, '占比%', COMPLIANCE_DOC_REF)
},
},
{
id: 'psur-province',
title: '省级 AE 报告数',
description: 'CMP-AE-601',
chartExplain: ex('不良事件表 province 字段;全期按省聚合。', 'CMP-AE-601省级报告条数分布用于地理聚集识别地图可视化可后续接入。'),
optionBuilder: (context) => {
const p = buildProvinceAeCounts(context)
return barOption('CMP-AE-601 省分布', p.labels, p.values, COMPLIANCE_DOC_REF)
},
},
{
id: 'psur-lag-histogram',
title: '上报滞后天数分布',
description: 'Lag = 审核日期 登记日期CMP-AE-701',
chartExplain: ex(
'AE 表 reviewDate、registrationDate 均可解析的行Lag=审核日期−登记日期(天)。',
'CMP-AE-701对滞后天数做直方分箱支撑上报时效分布与内控阈值讨论。',
),
optionBuilder: reportingLagHistogramOption,
},
],
},
],
},
'compliance-audit': {
key: 'compliance-audit',
title: '上报合规性审计',
pillar: 'diagnose',
strategyRef: '合规方向分析 §3',
subtitle: '诊断层CMP-CV-101~104 与 CMP-AE-701/702 双口径演示。',
sections: [
{
id: 'audit-core',
title: '审计核心指标',
strategyRefs: ['CMP-CV-101', 'CMP-CV-104', 'CMP-AE-702'],
charts: [
{
id: 'audit-summary-bar',
title: '漏报 / 匹配 / 时效(双口径)',
description: COMPLIANCE_FOOTNOTE_AUDIT,
chartExplain: ex(
'基准=投诉 isAe=true匹配规则=医院+产品+型号一致且 AE 发生日与 C3登记日相差≤30天。',
'汇总漏报率、匹配率及匹配子集上「审核登记」「审核C3」≤15 天占比(演示阈值)。',
),
optionBuilder: complianceAuditSummaryOption,
},
{
id: 'audit-counts-pie',
title: '应报样本结构',
description: '投诉 isAe=是',
chartExplain: ex(
'同上基准集合;在可匹配与疑似漏报二分结构上展示样本量。',
'饼图用于审计抽样框说明:已匹配 vs 需人工复核的疑似漏报。',
),
optionBuilder: complianceAuditCountsOption,
},
],
},
],
},
'quality-concentration': {
key: 'quality-concentration',
title: '投诉集中性与 Pareto',
pillar: 'diagnose',
strategyRef: '质量方向分析 §2§3',
subtitle: '诊断层QLT-PRD-401 / QLT-BTH-101 / QLT-CNC 系列(集中性)。',
sections: [
{
id: 'quality-pareto',
title: '集中性与批号',
strategyRefs: ['QLT-PRD-401', 'QLT-BTH-101', 'QLT-CNC-301~303'],
charts: [
{
id: 'quality-pareto-chart',
title: '产品×故障 Pareto二八',
description: 'QLT-PRD-401',
chartExplain: ex(
`质量投诉表;${QLT_TIME_AXIS};取 Top8 产品×故障组合。`,
'对「产品名称×故障类型」计数并降序;同时给出累计占比曲线,用于识别贡献约 80% 投诉的组合Pareto。',
),
optionBuilder: paretoOption,
},
{
id: 'quality-batch-chart',
title: '批号投诉条数排名',
description: 'QLT-BTH-101',
chartExplain: ex(
'质量投诉表 batchNo 字段;空批号归入「(未填批号)」桶。',
'在统计期内按批号分组对投诉条数计数并取 Top用于触发批次调查问题批次识别。',
),
optionBuilder: (context) => {
const batch = buildComplaintBatchTop(context, 8)
return barOption('批号投诉 Top', batch.labels, batch.values, QUALITY_DOC_REF)
},
},
{
id: 'quality-hospital-top',
title: '医院投诉条数 Top',
description: 'QLT-CNC-301',
chartExplain: ex(
'质量投诉表;按医院名称聚合。',
'统计各医院投诉条数并取 Top用于识别投诉集中医院现场调查/培训优先级)。',
),
optionBuilder: (context) => {
const t = buildTopHospitalsComplaints(context, 8)
return barOption('医院投诉 Top', t.labels, t.values, QUALITY_DOC_REF)
},
},
{
id: 'quality-product-fault-top',
title: '故障类型 Top',
description: 'QLT-CNC-303',
chartExplain: ex('质量投诉表 faultType 字段。', '按故障类型计数取 Top与 Pareto 组合图互补的一维缺陷雷达。'),
optionBuilder: (context) => {
const t = buildTopFaultTypes(context, 8)
return barOption('故障类型 Top', t.labels, t.values, QUALITY_DOC_REF)
},
},
],
},
],
},
'quality-trends': {
key: 'quality-trends',
title: '趋势分析',
pillar: 'monitor',
strategyRef: '质量方向分析 §2.2 / §4.1',
subtitle: '监测层月度趋势与上下半年对比、区域对比QLT-TRN-703/704。',
sections: [
{
id: 'trend-core',
title: '时间与对比窗',
strategyRefs: ['QLT-TRN-703', 'QLT-TRN-704'],
charts: [
{
id: 'trend-monthly',
title: '投诉月度趋势',
description: 'C3登记月',
chartExplain: ex(
`质量投诉表;${QLT_TIME_AXIS};按公历年月桶聚合。`,
'对每个自然月统计投诉条数,观察总量是否异常上升(与主策略 §3.2 趋势主题一致)。',
),
optionBuilder: complaintMonthlyLineOption,
},
{
id: 'trend-product-half',
title: '产品:上/下半年投诉量',
description: 'QLT-TRN-703',
chartExplain: ex(
'质量投诉表;以登记日期的月份区分上半年(16)与下半年(712)。',
'按产品对两个半年窗口分别计数,用于与增速预警表对照阅读。',
),
optionBuilder: productHalfYearBarOption,
},
{
id: 'trend-product-alert',
title: '产品投诉增速预警',
description: 'QLT-TRN-703',
chartExplain: ex(
'同上、对比窗为同一公历年的上下半年。',
'当 (下半年−上半年)/上半年 ≥ 20%(且上半年>0或「上半年零且下半年≥2」时纳入预警柱图。',
),
optionBuilder: productGrowthAlertBarOption,
},
{
id: 'trend-province-half',
title: '省份:上/下半年投诉量',
description: 'QLT-TRN-704',
chartExplain: ex(
'投诉侧按医院映射至省份:映射表由入院量数据中「医院→省」出现频次众数生成。',
'将投诉按登记月拆上下半年,在省级桶内计数,用于区域波动监测。',
),
optionBuilder: provinceHalfYearBarOption,
},
],
},
],
},
'quality-complaint-sales': {
key: 'quality-complaint-sales',
title: '投诉行为:销售与入院关联',
pillar: 'diagnose',
strategyRef: '质量方向分析 §4.3',
subtitle: '诊断层投诉量与入院量关系、医院×产品投诉率QLT-SAL-901。',
sections: [
{
id: 'qc-sales',
title: '入院与投诉率',
strategyRefs: ['QLT-SAL-901', '主策略 3.3 a'],
charts: [
{
id: 'qc-sales-scatter',
title: '医院:投诉条数 vs 入院 cyQty',
description: '散点',
chartExplain: ex(
'投诉按医院计数;入院量表按 HospitalName 汇总 cyQty跨月相加演示口径。',
'用于观察「入院量大是否必然投诉多」;偏离对角带可为异常医院信号(需结合床位/产品结构)。',
),
optionBuilder: hospitalComplaintQtyScatterOption,
},
{
id: 'qc-sales-rate',
title: '医院×产品 投诉率 Top每千件',
description: 'QLT-SAL-901',
chartExplain: ex(
`分子=统计期内医院+产品投诉条数;分母=入院量表同键 cyQty 汇总。${QLT_ADMISSION_DENOM}`,
'投诉率 = 投诉条数 / cyQty × 1000取 Top10 横向条形图,突出相对暴露下的高频组合。',
),
optionBuilder: complaintRateBarOption,
},
],
},
],
},
'quality-complaint-investigation': {
key: 'quality-complaint-investigation',
title: '投诉行为:调查与赔付关系',
pillar: 'act',
strategyRef: '质量方向分析 §3.3§3.4',
subtitle: '决策层调查结论池、缺陷成立组合、赔付结构QLT-INV / QLT-CST。',
sections: [
{
id: 'qc-investigation',
title: '调查与成本',
strategyRefs: ['QLT-INV-501', 'QLT-INV-502', 'QLT-CST-601'],
charts: [
{
id: 'qc-inv-pie',
title: '调查结论分布',
description: 'QLT-INV-501',
chartExplain: ex(
'质量投诉表 conclusion调查结论字段全量有效行。',
'对结论类别计数并计算占比,观察「产品缺陷成立」等结构是否恶化。',
),
optionBuilder: investigationPieOption,
},
{
id: 'qc-defect-bar',
title: '产品缺陷成立:产品×故障 Top',
description: 'QLT-INV-502',
chartExplain: ex(
'子集:调查结论=「产品缺陷成立」的投诉记录。',
'在子集内按「产品×故障」计数排序,形成已确认质量问题池,供 CAPA/设计变更引用。',
),
optionBuilder: defectProductFaultBarOption,
},
{
id: 'qc-comp-pie',
title: '赔付结论分布',
description: 'QLT-CST-601',
chartExplain: ex('质量投诉表 compensation赔付结论字段。', '统计各类赔付方式占比作为质量失败外显成本COQ结构的入口指标。'),
optionBuilder: compensationPieOption,
},
{
id: 'qc-product-top',
title: '产品投诉条数 Top',
description: 'QLT-CNC-302',
chartExplain: ex('质量投诉表 productName。', '按产品聚合投诉条数,与调查/赔付饼图联读以定位重点产品。'),
optionBuilder: (context) => {
const t = buildTopProductsComplaints(context, 8)
return barOption('产品投诉 Top', t.labels, t.values, QUALITY_DOC_REF)
},
},
],
},
],
},
'quality-complaint-batch': {
key: 'quality-complaint-batch',
title: '投诉行为:批次与稳定性',
pillar: 'diagnose',
strategyRef: '质量方向分析 §2',
subtitle: '诊断层:批号与 AE 双通道对照、关闭周期QLT-BTH-104 / QLT-RPT-802。',
sections: [
{
id: 'qc-batch',
title: '批次与闭环',
strategyRefs: ['QLT-BTH-104', 'QLT-RPT-802'],
charts: [
{
id: 'qc-batch-ae',
title: '批号:投诉 vs AE 条数',
description: 'QLT-BTH-104',
chartExplain: ex(
'投诉侧 batchNoAE 侧 productBatchbatchNo剔除双方均为 0 的批号。',
'同一批号字符串在投诉表 batchNo 与 AE 表 batchNo 上分别计数,用于市场反馈与监管报告双通道对照(非逐条匹配)。',
),
optionBuilder: batchComplaintAeBarOption,
},
{
id: 'qc-close-cycle',
title: '投诉关闭周期分布',
description: 'QLT-RPT-802',
chartExplain: ex(
'仅统计 closeDate、registerDate 均可解析且差分≥0 的投诉行。',
'关闭周期=关闭日期C3登记日期按天分段聚合为直方图用于流程效率与结案 SLA 讨论。',
),
optionBuilder: closeCycleHistogramOption,
},
],
},
],
},
'quality-complaint-text': {
key: 'quality-complaint-text',
title: '投诉行为:文本与流程类',
pillar: 'goal',
strategyRef: '3.3 p-x',
subtitle: '目标层,占位展示文本挖掘与流程效率方向。',
sections: [{ id: 'qc-text', title: '主题组 D', strategyRefs: ['3.3 p-x'], charts: [...placeholderPair('qc-text-a'), ...placeholderPair('qc-text-b')] }],
},
'quality-ae-cause': {
key: 'quality-ae-cause',
title: 'AE 报告原因推测',
pillar: 'diagnose',
strategyRef: '质量方向分析 §4.4',
subtitle: '诊断层AE 与入院量散点、按产品升级率QLT-AE-1001/1003。',
sections: [
{
id: 'ae-cause',
title: '报告行为与升级',
strategyRefs: ['QLT-AE-1003', 'QLT-AE-1001'],
charts: [
{
id: 'ae-scatter-hosp',
title: '医院AE 条数 vs 入院 cyQty',
description: 'QLT-AE-1003',
chartExplain: ex(
'AE 表按 unitName 计数报告条数;入院表按 HospitalName 汇总 cyQtyAE 未按投诉时间轴过滤。',
'用于发现「相对入院量异常高报告」的医院点;解读需结合合规口径(发生日期)与报告动机,不等同于质量差。',
),
optionBuilder: hospitalAeScatterOption,
},
{
id: 'ae-upgrade-rate',
title: '按产品的投诉升级率',
description: 'QLT-AE-1001',
chartExplain: ex(
'质量投诉表 isAe是否不良事件与 productName。',
'升级率 = 该产品 isAe=true 的投诉条数 / 该产品总投诉条数 ×100%,比较各产品线升级意愿差异。',
),
optionBuilder: isAeRateBarOption,
},
],
},
],
},
'quality-ae-only': {
key: 'quality-ae-only',
title: '报告不投诉分析',
pillar: 'monitor',
strategyRef: '3.4.2',
subtitle: '监测层AE 器械故障分布(演示)。',
sections: [
{
id: 'ae-only',
title: '仅 AE 报告',
strategyRefs: ['3.4.2'],
charts: [
{
id: 'ae-only-failure',
title: 'AE 器械故障表现分布',
description: '演示',
chartExplain: ex(
'不良事件表全量;字段 deviceFailure。',
'对仅走监管通道的报告,按器械故障表现聚类,辅助理解「未并发投诉」的事件形态(主策略 §3.4.2 占位深化方向)。',
),
optionBuilder: (context) => {
const top = buildTopDeviceFailures(context, 8)
return barOption('AE 故障 Top', top.labels, top.values, COMPLIANCE_DOC_REF)
},
},
{
id: 'ae-only-monthly',
title: 'AE 月度条数(发生日期)',
description: '演示',
chartExplain: ex('不良事件表 occurrenceDate 按月桶。', '与投诉月度趋势对照时须在业务上区分两条时间轴。'),
optionBuilder: (context) => {
const t = buildAeMonthlyByOccurrence(context)
return lineOption('AE 月度', t.months, t.values, 'AE 条数', COMPLIANCE_DOC_REF)
},
},
],
},
],
},
'quality-ae-with-complaint': {
key: 'quality-ae-with-complaint',
title: '报告且投诉分析',
pillar: 'diagnose',
strategyRef: '3.4.3',
subtitle: '诊断层已升级投诉isAe=true的故障类型分布演示。',
sections: [
{
id: 'ae-with-complaint',
title: '报告+投诉交集',
strategyRefs: ['3.4.3'],
charts: [
{
id: 'ae-with-fault',
title: '已升级投诉的故障类型分布',
description: 'isAe=true',
chartExplain: ex(
'子集:质量投诉表 isAe=true。',
'在「已升级」子集上按 faultType 计数,用于与 AE 表伤害/故障矩阵叙事衔接(细粒度匹配规则见合规审计主题)。',
),
optionBuilder: (context) => {
const sub = context.complaints.filter((c) => c.isAe)
const m = new Map<string, number>()
sub.forEach((c) => m.set(c.faultType, (m.get(c.faultType) ?? 0) + 1))
const sorted = [...m.entries()].sort((a, b) => b[1] - a[1])
return barOption(
'升级投诉故障类型',
sorted.map((i) => i[0]),
sorted.map((i) => i[1]),
QUALITY_DOC_REF,
)
},
},
{
id: 'ae-with-monthly',
title: '已升级投诉的登记月份趋势',
description: 'C3登记月',
chartExplain: ex('子集isAe=true时间字段 registerDate。', '观察升级投诉在登记时间上的分布,用于与 AE 发生月趋势对照。'),
optionBuilder: (context) => {
const sub = context.complaints.filter((c) => c.isAe)
const counter = new Map<string, number>()
sub.forEach((c) => {
const k = c.registerDate.slice(0, 7)
counter.set(k, (counter.get(k) ?? 0) + 1)
})
const months = [...counter.keys()].sort()
return lineOption('升级投诉月度', months, months.map((x) => counter.get(x) ?? 0), '条数', QUALITY_DOC_REF)
},
},
],
},
],
},
'marketing-operation-training': {
key: 'marketing-operation-training',
title: '操作不当与培训',
pillar: 'act',
strategyRef: '营销方向分析 §2',
subtitle: '决策层MKT-OP-101~104支撑医院清单、经销商辅导与培训素材优先级。',
sections: [
{
id: 'marketing-operation',
title: '培训与使用支持',
strategyRefs: ['MKT-OP-101', 'MKT-OP-102', 'MKT-OP-103', 'MKT-OP-104'],
charts: [
{
id: 'mkt-op-hospital',
title: '操作不当投诉:医院分布',
description: 'MKT-OP-101',
chartExplain: ex(
`投诉表子集:调查结论=「操作不当」;${MKT_TIME_AXIS};全量 mock。`,
'按医院名称计数操作不当投诉条数并取 Top用于院内培训与跟台优先级主策略 §4.1)。',
),
optionBuilder: mktOpWrongHospitalOption,
},
{
id: 'mkt-op-dealer-count',
title: '操作不当投诉:经销商条数',
description: 'MKT-OP-102',
chartExplain: ex(
`同上子集;${MKT_HOSPITAL_MAP} 将投诉归入经销商后计数。`,
'识别操作不当投诉绝对量高的经销商,作为渠道侧约谈与联合巡院名单输入。',
),
optionBuilder: mktOpWrongDealerCountOption,
},
{
id: 'mkt-op-dealer-rate',
title: '经销商操作不当率(入院量分母)',
description: 'MKT-OP-103',
chartExplain: ex(
`分子=各经销商子集内操作不当投诉条数;分母=入院量表同 DealerName 的 CY Qty 汇总;${MKT_ADMISSION_DENOM}`,
'率=分子/分母×1000件/千件),用于发现「相对销量操作不当偏多」的经销商(主策略 §4.1)。',
),
optionBuilder: mktDealerOpWrongRateOption,
},
{
id: 'mkt-op-product-share',
title: '产品:操作不当占比',
description: 'MKT-OP-104',
chartExplain: ex(
'全量投诉按产品计数分母;分子为该产品调查结论=操作不当的条数。',
'占比=操作不当条数/该产品投诉总条数×100%,用于说明书、操作视频与数字化推广素材的迭代优先级。',
),
optionBuilder: mktProductOpWrongShareOption,
},
],
},
],
},
'marketing-region-dealer': {
key: 'marketing-region-dealer',
title: '区域与经销商投诉率',
pillar: 'monitor',
strategyRef: '营销方向分析 §3',
subtitle: '监测层MKT-RG-201~203区分相对率与绝对量。',
sections: [
{
id: 'marketing-region',
title: '区域与渠道',
strategyRefs: ['MKT-RG-201', 'MKT-RG-202', 'MKT-RG-203'],
charts: [
{
id: 'mkt-rg-province-rate',
title: '省级投诉率(件/千件入院量)',
description: 'MKT-RG-201',
chartExplain: ex(
`投诉经医院映射至省(${MKT_HOSPITAL_MAP});分母为入院量同省 CY Qty 汇总;${MKT_TIME_AXIS}`,
'投诉率=投诉条数/入院量×1000用于区域活动与资源排序主策略 §4.2)。',
),
optionBuilder: mktProvinceComplaintRateOption,
},
{
id: 'mkt-rg-dealer-rate',
title: '经销商投诉率(件/千件入院量)',
description: 'MKT-RG-202',
chartExplain: ex(
`投诉经医院映射至经销商众数;分母为入院量表 DealerName 维度 CY Qty 汇总。`,
'用于渠道储运、覆盖密度与客户支持资源的再分配假设验证(主策略 §4.2)。',
),
optionBuilder: mktDealerComplaintRateOption,
},
{
id: 'mkt-rg-province-count',
title: '省级投诉条数 Top绝对量',
description: 'MKT-RG-203',
chartExplain: ex(
'全量投诉按省计数,不按入院量标准化。',
'与省级投诉率对照,区分「人口/销量大省带来的绝对量大」与「相对率异常高」。',
),
optionBuilder: mktProvinceComplaintCountOption,
},
],
},
],
},
'substance-matrix': {
key: 'substance-matrix',
title: '伤害 × 故障矩阵',
pillar: 'diagnose',
strategyRef: '合规方向分析 §2.4 / 主策略 5.1',
subtitle: '诊断层CMP-AE-401 全量与 CMP-AE-402 SAE 子集对照。',
sections: [
{
id: 'substance-matrix',
title: '关联矩阵',
strategyRefs: ['CMP-AE-401', 'CMP-AE-402'],
charts: [
{
id: 'matrix-all',
title: '伤害 × 器械故障(全量 AE',
description: COMPLIANCE_FOOTNOTE_SHORT,
chartExplain: ex(
'不良事件表 injuryExpression × deviceFailure时间过滤默认全量可按发生日期窗扩展。',
'CMP-AE-401二维计数矩阵识别高频伤害-故障组合以支撑 CAPA 优先级。',
),
optionBuilder: injuryDeviceHeatmapOption(
false,
'CMP-AE-401 伤害-故障热力图',
`${COMPLIANCE_DOC_REF}|按发生日期纳入全集`,
),
},
{
id: 'matrix-sae',
title: '伤害 × 器械故障SAE 子集)',
description: 'CMP-AE-402',
chartExplain: ex(
'在 SAE 子集(四严重标准任一)上重复 CMP-AE-401 的矩阵统计。',
'CMP-AE-402聚焦严重结局组合模式用于紧急医学评估与对外报告要点。',
),
optionBuilder: injuryDeviceHeatmapOption(
true,
'CMP-AE-402 SAE 子集',
'死亡/危及生命/残疾/住院或延长住院 任一',
),
},
],
},
],
},
'substance-seasonality': {
key: 'substance-seasonality',
title: '故障季节性分析',
pillar: 'monitor',
strategyRef: '主策略 §5.2 / 质量方向分析 §5',
subtitle: '监测层:投诉侧「故障类型×登记月」热力图(与质量方向月度桶一致)。',
sections: [
{
id: 'substance-seasonality',
title: '季节性模式',
strategyRefs: ['5.2', 'QLT-BTH-203'],
charts: [
{
id: 'seasonality-heatmap',
title: '故障类型 × C3登记月',
description: '投诉热力',
chartExplain: ex(
'质量投诉表 faultType × registerDate 公历月份;全量 mock。',
'单元格=该月该故障类型的投诉条数;颜色越深投诉越多,用于观察高温/雨季等是否与某类故障同向波动(探索性,需外部气候数据佐证)。',
),
optionBuilder: faultSeasonalityHeatmapOption,
},
{
id: 'seasonality-monthly-total',
title: '投诉月度总量(对照)',
description: 'QLT 时间轴',
chartExplain: ex('质量投诉表 registerDate 按月聚合。', '给出各月投诉总量曲线,与热力图行合计一致,便于读趋势峰值月份。'),
optionBuilder: complaintMonthlyLineOption,
},
],
},
],
},
'portfolio-persona': {
key: 'portfolio-persona',
title: '安全维度客户画像',
pillar: 'goal',
strategyRef: '6',
subtitle: '目标层,占位展示高投诉高复购客户画像。',
sections: [{ id: 'portfolio-persona', title: '客户画像', strategyRefs: ['6'], charts: placeholderPair('portfolio-persona') }],
},
'portfolio-integrated': {
key: 'portfolio-integrated',
title: '综合联动分析',
pillar: 'act',
strategyRef: '7',
subtitle: '决策层,支撑监管风险热区与优先级矩阵。',
sections: [{ id: 'portfolio-integrated', title: '综合联动', strategyRefs: ['7'], charts: placeholderPair('portfolio-integrated') }],
},
}
export function getAnalysisDefinition(key: string): AnalysisPageDefinition | undefined {
return configs[key]
}