| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229 |
- /* ==============================================================
- * 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
- }
|