/** * 算法检测器 JSON(单机图 / 风场总图)→ 前端与报告共用的面板结构。 * 单机:通用 data[],或多面板 panels[]。 * 风场:chartType=series 曲线叠加,chartType=value 单值柱状对比。 */ export const PO_TO_TEMPLATE_KEY = { anomalyPowercurvePO: "wind_power_curve", anomalyScatterPO: "wind_power_scatter", anomalyStaticyawPO: "yaw_static", anomalyCabletwistPO: "yaw_twist", anomalyYawErrorPO: "yaw_error", anomalyYawCountPO: "yaw_count", anomalyPitchcoordPO: "pitch_coord", anomalyPitchregulationPO: "pitch_regulation", anomalyMinpitchPO: "pitch_min", anomalyPowerqualityPO: "ctrl_power_quality", anomalyOperationPO: "ctrl_op_state", anomalyDeloadPO: "ctrl_deload", anomalyCpPO: "aero_cp", anomalyTsrPO: "aero_tsr", anomalyCpTsrPO: "aero_tsr_wind", }; export const FARM_VALUE_AXIS = { yaw_error: "静态偏航误差 (°)", yaw_count: "偏航次数", pitch_min: "最小桨距角漂移 (°)", }; export const FARM_VALUE_TITLES = { yaw_error: "全场静态偏航误差", yaw_count: "全场偏航次数", pitch_min: "全场最小桨距角漂移", }; export const FARM_SERIES_TITLES = { wind_power_curve: "全场功率曲线分析", wind_power_scatter: "全场风功率散点分析", yaw_static: "全场静态偏航异常分布", yaw_twist: "全场扭缆分析", pitch_regulation: "全场变桨一致性分析", pitch_coord: "全场变桨协调分析", ctrl_power_quality: "全场电能质量分析", ctrl_op_state: "全场运行状态分析", ctrl_deload: "全场降载判定分析", aero_cp: "全场Cp功率系数分析", aero_tsr: "全场TSR叶尖速比分析", aero_tsr_wind: "全场TSR-Cp联合分析", }; export function resolveTemplateKey(options = {}, detectorKey = "") { if (options.templateKey) return options.templateKey; const key = options.detectorKey || options.poKey || detectorKey || ""; return PO_TO_TEMPLATE_KEY[key] || options.detectorName || ""; } export function isYawErrorDetector(templateKey) { return templateKey === "yaw_error"; } export function isYawCountDetector(templateKey) { return templateKey === "yaw_count"; } export function isPitchMinDetector(templateKey) { return templateKey === "pitch_min"; } export function isFarmAggregateJson(plotJson) { return Boolean( plotJson && (plotJson.chartType === "series" || plotJson.chartType === "value") && Array.isArray(plotJson.turbines), ); } export function isValueBarPanel(panel) { return panel?.chartKind === "valueBar"; } export function isBubbleColorbar(row = {}, templateKey = "") { if (isPitchMinDetector(templateKey)) return true; return /点数/.test(String(row.colorbarTitle || row.colorbartitle || "")); } export function findPeakCoord(xArr = [], yArr = []) { const len = Math.min(xArr.length, yArr.length); if (!len) return null; let maxIdx = 0; let maxY = Number(yArr[0]); for (let i = 1; i < len; i += 1) { const y = Number(yArr[i]); if (Number.isFinite(y) && (!Number.isFinite(maxY) || y > maxY)) { maxY = y; maxIdx = i; } } if (!Number.isFinite(maxY)) return null; return { x: xArr[maxIdx], y: maxY, index: maxIdx }; } export function formatPcaSubtitle(panel = {}) { const defs = Array.isArray(panel.componentDefinition) ? panel.componentDefinition : []; if (!defs.length) return ""; return defs .map((item) => { const ratio = item?.explainedVarianceRatio != null ? ` ${(Number(item.explainedVarianceRatio) * 100).toFixed(1)}%` : ""; const loads = (item?.loadings || []) .slice(0, 4) .map((load) => { const name = load.displayName || load.feature || ""; const coef = Number(load.coefficient); return `${name}${Number.isFinite(coef) ? `:${coef.toFixed(2)}` : ""}`; }) .filter(Boolean) .join(", "); return `${item.name || "PC"}${ratio}${loads ? ` (${loads})` : ""}`; }) .join(" | "); } function copyPanelMeta(panel, plotJson = {}) { return { title: panel.panelTitle || panel.title || plotJson.title || "", xaxis: panel.xaxis || plotJson.xaxis || "", yaxis: panel.yaxis || plotJson.yaxis || "", xrange: panel.xrange || plotJson.xrange, yrange: panel.yrange || plotJson.yrange, data: panel.data || [], componentDefinition: panel.componentDefinition, rawMeans: panel.rawMeans, detectorName: plotJson.detector_name || plotJson.analysisType || plotJson.detector || "", }; } export function unwrapFarmPlotJson(raw) { if (!raw || typeof raw !== "object") return null; if (raw.__imageUrl) return raw; if (isFarmAggregateJson(raw)) return raw; if (raw.data && isFarmAggregateJson(raw.data)) return raw.data; return raw; } export function extractDetectorPanels(plotJson, options = {}) { const json = unwrapFarmPlotJson(plotJson); if (!json) return []; if (Array.isArray(json.panels) && json.panels.length) { return json.panels.map((panel) => copyPanelMeta(panel, json)); } if (isFarmAggregateJson(json)) { return farmAggregateToPanels(json, options); } return [copyPanelMeta(json, json)]; } function formatTurbineLabel(name, nameMap = {}) { const raw = String(name || ""); if (!raw) return "--"; const mapped = nameMap[raw] || nameMap[String(raw)]; const text = mapped ? String(mapped) : raw; if (/号$/.test(text)) return text; if (/^\d+$/.test(text)) return `${text}号`; return text; } function meanOf(values = []) { const nums = values.map(Number).filter((n) => Number.isFinite(n)); if (!nums.length) return null; return nums.reduce((sum, n) => sum + n, 0) / nums.length; } function farmSeriesMode(detector, item = {}) { if (item.xType === "time") return "markers"; if (detector === "wind_power_curve") return "lines"; if (detector === "pitch_regulation") return "lines+markers"; return "markers"; } function farmSeriesKind(item = {}) { if (item.status) return String(item.status); if (item.anomaly) return "异常"; return "正常"; } function sliceByFlag(item, flags, keepAnomaly) { const xData = []; const yData = []; const timeData = []; const sourceTime = Array.isArray(item.timeData) ? item.timeData : []; (item.xData || []).forEach((x, idx) => { const flagged = Boolean(flags[idx]); if (flagged !== keepAnomaly) return; xData.push(x); yData.push((item.yData || [])[idx]); if (sourceTime[idx] != null) timeData.push(sourceTime[idx]); }); return { xData, yData, timeData }; } function buildFarmSeriesRows(item, detector, nameMap) { const turbineName = formatTurbineLabel(item.turbine, nameMap); const mode = farmSeriesMode(detector, item); const flags = Array.isArray(item.pointAnomaly) ? item.pointAnomaly : []; const canSplit = flags.length && flags.length === (item.xData || []).length; const rows = canSplit ? [ { kind: "正常", sliced: sliceByFlag(item, flags, false) }, { kind: "异常", sliced: sliceByFlag(item, flags, true) }, ].filter((row) => row.sliced.xData.length) : [{ kind: farmSeriesKind(item), sliced: null }]; return rows.map((row) => { const sliced = row.sliced; return { label: `${turbineName} ${row.kind}`, originalLabel: row.kind, mode, xData: sliced ? sliced.xData : item.xData || [], yData: sliced ? sliced.yData : item.yData || [], timeData: sliced ? sliced.timeData : item.xType === "time" ? item.xData || [] : [], pointAnomaly: canSplit ? undefined : item.pointAnomaly, __farm: true, anomaly: Boolean(item.anomaly), extra: item, }; }); } export function farmAggregateToPanels(farmJson = {}, options = {}) { const detector = farmJson.detector || options.templateKey || ""; const turbines = Array.isArray(farmJson.turbines) ? farmJson.turbines : []; const nameMap = options.nameMap || {}; const title = options.title || FARM_VALUE_TITLES[detector] || FARM_SERIES_TITLES[detector] || farmJson.farm || "全场检测汇总"; if (farmJson.chartType === "value") { const values = turbines.map((item) => Number(item.value)); return [ { chartKind: "valueBar", title, detectorName: detector, xaxis: "风机", yaxis: FARM_VALUE_AXIS[detector] || "检测值", categories: turbines.map((item) => formatTurbineLabel(item.turbine, nameMap), ), values, anomalyFlags: turbines.map((item) => Boolean(item.anomaly)), extras: turbines, mean: meanOf(values), }, ]; } return [ { title, xaxis: farmJson.xaxis || "", yaxis: farmJson.yaxis || "", xrange: farmJson.xrange, yrange: farmJson.yrange, detectorName: detector, data: turbines.flatMap((item, turbineIndex) => buildFarmSeriesRows(item, detector, nameMap).map((row) => ({ ...row, turbineIndex, engineName: formatTurbineLabel(item.turbine, nameMap), })), ), }, ]; } export function formatPointHover(point) { if (!point || typeof point !== "object") return ""; const lines = []; if (point.dataTime) lines.push(`时间: ${point.dataTime}`); const appendMap = (obj, prefix) => { if (!obj || typeof obj !== "object") return; Object.keys(obj).forEach((key) => { const val = obj[key]; if (val == null || val === "") return; lines.push(`${prefix}${key}: ${val}`); }); }; appendMap(point.raw, ""); appendMap(point.deviation, "偏差 "); appendMap(point.mean, "均值 "); appendMap(point.meanDeviation, "均值偏差 "); return lines.join("
"); } export function formatFarmSeriesHover(extra = {}, detector = "") { if (!extra || typeof extra !== "object") return ""; const lines = []; const push = (label, val, suffix = "") => { if (val == null || val === "") return; lines.push(`${label}: ${val}${suffix}`); }; push("状态", extra.status); push("发电量比值", extra.ratio); push("异常点数", extra.anomalyPoints); push("降载占比", extra.deloadRatio); if (detector === "yaw_error") { push("误差", extra.value, "°"); push("发电量损失", extra.powerloss, "%"); push("覆盖率", extra.coverage); } if (detector === "yaw_count") { push("CW", extra.cw); push("CCW", extra.ccw); push("覆盖率", extra.coverage); } if (detector === "pitch_min") { push("P10", extra.p10, "°"); push("基线", extra.baseline, "°"); } return lines.join("
"); } export function formatFarmValueHover(extra = {}, detector = "") { if (!extra || typeof extra !== "object") return ""; const lines = []; const push = (label, val, suffix = "") => { if (val == null || val === "") return; lines.push(`${label}: ${val}${suffix}`); }; if (detector === "yaw_error") { push("误差", extra.value, "°"); push("发电量损失", extra.powerloss, "%"); push("覆盖率", extra.coverage != null ? Number(extra.coverage) : extra.coverage); } else if (detector === "yaw_count") { push("次数", extra.value); push("CW", extra.cw); push("CCW", extra.ccw); push("覆盖率", extra.coverage); } else if (detector === "pitch_min") { push("漂移", extra.value, "°"); push("P10", extra.p10, "°"); push("基线", extra.baseline, "°"); } else { push("数值", extra.value); } return lines.join("
"); } export function scaleBubbleSize(value, min, max, { minSize = 8, maxSize = 28 } = {}) { const num = Number(value); if (!Number.isFinite(num)) return minSize; if (!Number.isFinite(min) || !Number.isFinite(max) || max <= min) return 14; const ratio = (num - min) / (max - min); return minSize + ratio * (maxSize - minSize); }