/**
* 算法检测器 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);
}