/* ============================================================== * utils/indicator-timeline.js - 指标时间轴纯函数 * 规则来源:design §6.6 / PRD §5.3 / §4.2 * 职责:chartMode 消费、X 轴抽稀、日期格式化、缺失期合并 * 约束:所有日期解析走 utils/format.js 的 parseDate,禁 new Date(string) * ============================================================== */ import { parseDate } from './format.js' /** * 将 points ∪ missingPeriods 合并为等距 X 轴日期全集(升序) * @param {Array} points [{ reportDate, ... }] * @param {Array} missing [{ reportDate, ... }] * @returns {string[]} 排序后的不重复日期字符串 'yyyy-MM-dd' */ export function buildAllPeriodDates(points, missing) { var set = {} var i if (Array.isArray(points)) { for (i = 0; i < points.length; i++) { var d = points[i] && points[i].reportDate if (d) set[d] = true } } if (Array.isArray(missing)) { for (i = 0; i < missing.length; i++) { var d = missing[i] && missing[i].reportDate if (d) set[d] = true } } return Object.keys(set).sort() } /** * 把期数映射为期次明细表行(倒序,最新在最上),每行带相对更早一期的 Δ * 缺失期(missingPeriods)也并入表内,显示「该期未检测」(灰,不连线) * @param {Array} points 按时间升序的观测点 * @param {Array} missing 缺失期 [{ reportDate, reportId, dateSource }] * @returns {Array} 按时间倒序的 rows */ export function buildPeriodRows(points, missing) { var pts = Array.isArray(points) ? points.slice() : [] // 按日期升序(走 parseDate,禁 new Date(string)) pts.sort(function(a, b) { var ta = parseDate(a && a.reportDate) var tb = parseDate(b && b.reportDate) if (!ta && !tb) return 0 if (!ta) return 1 if (!tb) return -1 return ta.getTime() - tb.getTime() }) // 每个有效点相对「更早一期有效点」的 Δ var deltaMap = {} for (var i = 0; i < pts.length; i++) { var cur = parseNum(pts[i].numericValue !== undefined ? pts[i].numericValue : pts[i].value) var prev = i > 0 ? parseNum(pts[i - 1].numericValue !== undefined ? pts[i - 1].numericValue : pts[i - 1].value) : NaN if (!isNaN(cur) && !isNaN(prev)) { var d = cur - prev deltaMap[pts[i].reportDate] = { delta: d, deltaPct: prev === 0 ? null : (d / prev * 100), trendLabel: d > 0 ? '升高' : (d < 0 ? '降低' : '持平') } } else { deltaMap[pts[i].reportDate] = { delta: null, deltaPct: null, trendLabel: '首次记录' } } } // 合并有效点 + 缺失期,倒序 var all = [] for (i = 0; i < pts.length; i++) { all.push({ date: pts[i].reportDate, point: pts[i], missing: false }) } var mis = Array.isArray(missing) ? missing : [] for (i = 0; i < mis.length; i++) { if (!mis[i] || !mis[i].reportDate) continue all.push({ date: mis[i].reportDate, point: null, missing: true, dateSource: mis[i].dateSource }) } all.sort(function(a, b) { var ta = parseDate(a.date) var tb = parseDate(b.date) if (!ta && !tb) return 0 if (!ta) return 1 if (!tb) return -1 return tb.getTime() - ta.getTime() }) var rows = [] for (i = 0; i < all.length; i++) { var item = all[i] if (item.missing) { rows.push({ reportDate: item.date, dateSource: item.dateSource || '', value: '', unit: '', status: '', isMissing: true, delta: null, deltaPct: null, trendLabel: '该期未检测', deltaText: '—' }) } else { var p = item.point var dm = deltaMap[p.reportDate] || { delta: null, deltaPct: null, trendLabel: '首次记录' } rows.push({ reportDate: p.reportDate, dateSource: p.dateSource || '', value: p.value, numericValue: p.numericValue, unit: p.unit, status: p.status, isMissing: false, delta: dm.delta, deltaPct: dm.deltaPct, trendLabel: dm.trendLabel, deltaText: formatRowDelta(dm.delta, dm.deltaPct) }) } } return rows } /** 期次行的 Δ 文本:+1.23 (+10.0%) / -0.48 / — */ function formatRowDelta(delta, deltaPct) { if (delta === null || delta === undefined || isNaN(delta)) return '—' var sign = delta > 0 ? '+' : '' var d = roundSig(delta) if (deltaPct === null || deltaPct === undefined || isNaN(deltaPct)) { return sign + d + ' (—)' } var psign = deltaPct > 0 ? '+' : '' return sign + d + ' (' + psign + roundPct(deltaPct) + '%)' } /** 有效数字(§6.4):≥100 整数;10–100 一位小数;<10 两位小数 */ export function roundSig(v) { var a = Math.abs(v) if (a >= 100) return String(Math.round(v)) if (a >= 10) return v.toFixed(1) return v.toFixed(2) } function roundPct(v) { return v.toFixed(1) } function parseNum(v) { var n = Number(v) return isNaN(n) ? NaN : n } /** * 报告日期展示:MM-DD(同年时省略年份) * @param {string} dateStr 'yyyy-MM-dd' * @param {string} refDateStr 'yyyy-MM-dd' 用于判断同年 */ export function formatPeriodDate(dateStr, refDateStr) { if (!dateStr) return '--' var s = String(dateStr) if (s.length >= 10) { var ymd = s.slice(0, 10) if (refDateStr && String(refDateStr).slice(0, 4) === ymd.slice(0, 4)) { return ymd.slice(5) } return ymd } return s } /** * chartMode 消费:判定是否画图、是否标值、标签模式(消费后端 chartMode,不自行判断) * @param {string} chartMode 'empty'|'single'|'pair'|'line'|'line_aggregated' * @param {number} n 期数 * @returns {{draw:boolean, showValues:boolean, labelMode:'none'|'all'|'sparse'}} */ export function consumeChartMode(chartMode, n) { if (chartMode === 'empty' || chartMode === 'single') return { draw: false, showValues: false, labelMode: 'none' } if (chartMode === 'pair') return { draw: false, showValues: true, labelMode: 'all' } if (chartMode === 'line_aggregated') return { draw: true, showValues: false, labelMode: 'sparse' } if (chartMode === 'line') return { draw: true, showValues: n <= 8, labelMode: n <= 8 ? 'all' : 'sparse' } // 兜底:后端未下发 chartMode 时,按期数保守判定(不影响已下发场景) if (!n || n < 2) return { draw: false, showValues: false, labelMode: 'none' } if (n === 2) return { draw: false, showValues: true, labelMode: 'all' } return { draw: true, showValues: n <= 8, labelMode: n <= 8 ? 'all' : 'sparse' } } /** X 轴标签下标抽稀(§5.2 选型表) */ export function axisLabelIndices(n) { if (!n || n <= 8) { var all = []; for (var i = 0; i < n; i++) all.push(i); return all } if (n <= 19) return uniqueIdx([0, Math.floor((n - 1) / 3), Math.floor(2 * (n - 1) / 3), n - 1], n) return uniqueIdx([0, Math.floor(n / 2), n - 1], n) } function uniqueIdx(arr, n) { var out = [], seen = {} for (var i = 0; i < arr.length; i++) { var idx = arr[i] if (idx < 0) idx = 0 if (idx >= n) idx = n - 1 if (!seen[idx]) { seen[idx] = true; out.push(idx) } } out.sort(function(a, b) { return a - b }) return out } /** 日期字符串归一化为 'yyyy-MM-dd'(走 parseDate,禁 new Date(string)) */ export function normalizeDate(dateStr) { if (!dateStr) return '' var d = parseDate(dateStr) if (!d) return String(dateStr).slice(0, 10) var y = d.getFullYear() var m = (d.getMonth() + 1).toString().padStart ? (d.getMonth() + 1).toString().padStart(2, '0') : ('0' + (d.getMonth() + 1)).slice(-2) var day = d.getDate().toString().padStart ? d.getDate().toString().padStart(2, '0') : ('0' + d.getDate()).slice(-2) return y + '-' + m + '-' + day } /** 上传日标记:dateSource 非 real/unknown 时,日期后加 * */ export function dateWithSourceMark(dateStr, dateSource) { var star = (dateSource && dateSource !== 'real' && dateSource !== 'unknown') ? ' *' : '' return String(dateStr || '') + star } /** 单位不一致标记:值后加 * */ export function unitMismatchMark(value, unit, latestUnit, isMismatch) { var s = String(value == null ? '--' : value) if (unit) s += ' ' + unit if (isMismatch) s += ' *' return s }