anomalyDetectorPlotAdapter.mjs 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368
  1. /**
  2. * 算法检测器 JSON(单机图 / 风场总图)→ 前端与报告共用的面板结构。
  3. * 单机:通用 data[],或多面板 panels[]。
  4. * 风场:chartType=series 曲线叠加,chartType=value 单值柱状对比。
  5. */
  6. export const PO_TO_TEMPLATE_KEY = {
  7. anomalyPowercurvePO: "wind_power_curve",
  8. anomalyScatterPO: "wind_power_scatter",
  9. anomalyStaticyawPO: "yaw_static",
  10. anomalyCabletwistPO: "yaw_twist",
  11. anomalyYawErrorPO: "yaw_error",
  12. anomalyYawCountPO: "yaw_count",
  13. anomalyPitchcoordPO: "pitch_coord",
  14. anomalyPitchregulationPO: "pitch_regulation",
  15. anomalyMinpitchPO: "pitch_min",
  16. anomalyPowerqualityPO: "ctrl_power_quality",
  17. anomalyOperationPO: "ctrl_op_state",
  18. anomalyDeloadPO: "ctrl_deload",
  19. anomalyCpPO: "aero_cp",
  20. anomalyTsrPO: "aero_tsr",
  21. anomalyCpTsrPO: "aero_tsr_wind",
  22. };
  23. export const FARM_VALUE_AXIS = {
  24. yaw_error: "静态偏航误差 (°)",
  25. yaw_count: "偏航次数",
  26. pitch_min: "最小桨距角漂移 (°)",
  27. };
  28. export const FARM_VALUE_TITLES = {
  29. yaw_error: "全场静态偏航误差",
  30. yaw_count: "全场偏航次数",
  31. pitch_min: "全场最小桨距角漂移",
  32. };
  33. export const FARM_SERIES_TITLES = {
  34. wind_power_curve: "全场功率曲线分析",
  35. wind_power_scatter: "全场风功率散点分析",
  36. yaw_static: "全场静态偏航异常分布",
  37. yaw_twist: "全场扭缆分析",
  38. pitch_regulation: "全场变桨一致性分析",
  39. pitch_coord: "全场变桨协调分析",
  40. ctrl_power_quality: "全场电能质量分析",
  41. ctrl_op_state: "全场运行状态分析",
  42. ctrl_deload: "全场降载判定分析",
  43. aero_cp: "全场Cp功率系数分析",
  44. aero_tsr: "全场TSR叶尖速比分析",
  45. aero_tsr_wind: "全场TSR-Cp联合分析",
  46. };
  47. export function resolveTemplateKey(options = {}, detectorKey = "") {
  48. if (options.templateKey) return options.templateKey;
  49. const key = options.detectorKey || options.poKey || detectorKey || "";
  50. return PO_TO_TEMPLATE_KEY[key] || options.detectorName || "";
  51. }
  52. export function isYawErrorDetector(templateKey) {
  53. return templateKey === "yaw_error";
  54. }
  55. export function isYawCountDetector(templateKey) {
  56. return templateKey === "yaw_count";
  57. }
  58. export function isPitchMinDetector(templateKey) {
  59. return templateKey === "pitch_min";
  60. }
  61. export function isFarmAggregateJson(plotJson) {
  62. return Boolean(
  63. plotJson &&
  64. (plotJson.chartType === "series" || plotJson.chartType === "value") &&
  65. Array.isArray(plotJson.turbines),
  66. );
  67. }
  68. export function isValueBarPanel(panel) {
  69. return panel?.chartKind === "valueBar";
  70. }
  71. export function isBubbleColorbar(row = {}, templateKey = "") {
  72. if (isPitchMinDetector(templateKey)) return true;
  73. return /点数/.test(String(row.colorbarTitle || row.colorbartitle || ""));
  74. }
  75. export function findPeakCoord(xArr = [], yArr = []) {
  76. const len = Math.min(xArr.length, yArr.length);
  77. if (!len) return null;
  78. let maxIdx = 0;
  79. let maxY = Number(yArr[0]);
  80. for (let i = 1; i < len; i += 1) {
  81. const y = Number(yArr[i]);
  82. if (Number.isFinite(y) && (!Number.isFinite(maxY) || y > maxY)) {
  83. maxY = y;
  84. maxIdx = i;
  85. }
  86. }
  87. if (!Number.isFinite(maxY)) return null;
  88. return { x: xArr[maxIdx], y: maxY, index: maxIdx };
  89. }
  90. export function formatPcaSubtitle(panel = {}) {
  91. const defs = Array.isArray(panel.componentDefinition)
  92. ? panel.componentDefinition
  93. : [];
  94. if (!defs.length) return "";
  95. return defs
  96. .map((item) => {
  97. const ratio =
  98. item?.explainedVarianceRatio != null
  99. ? ` ${(Number(item.explainedVarianceRatio) * 100).toFixed(1)}%`
  100. : "";
  101. const loads = (item?.loadings || [])
  102. .slice(0, 4)
  103. .map((load) => {
  104. const name = load.displayName || load.feature || "";
  105. const coef = Number(load.coefficient);
  106. return `${name}${Number.isFinite(coef) ? `:${coef.toFixed(2)}` : ""}`;
  107. })
  108. .filter(Boolean)
  109. .join(", ");
  110. return `${item.name || "PC"}${ratio}${loads ? ` (${loads})` : ""}`;
  111. })
  112. .join(" | ");
  113. }
  114. function copyPanelMeta(panel, plotJson = {}) {
  115. return {
  116. title: panel.panelTitle || panel.title || plotJson.title || "",
  117. xaxis: panel.xaxis || plotJson.xaxis || "",
  118. yaxis: panel.yaxis || plotJson.yaxis || "",
  119. xrange: panel.xrange || plotJson.xrange,
  120. yrange: panel.yrange || plotJson.yrange,
  121. data: panel.data || [],
  122. componentDefinition: panel.componentDefinition,
  123. rawMeans: panel.rawMeans,
  124. detectorName: plotJson.detector_name || plotJson.analysisType || plotJson.detector || "",
  125. };
  126. }
  127. export function unwrapFarmPlotJson(raw) {
  128. if (!raw || typeof raw !== "object") return null;
  129. if (raw.__imageUrl) return raw;
  130. if (isFarmAggregateJson(raw)) return raw;
  131. if (raw.data && isFarmAggregateJson(raw.data)) return raw.data;
  132. return raw;
  133. }
  134. export function extractDetectorPanels(plotJson, options = {}) {
  135. const json = unwrapFarmPlotJson(plotJson);
  136. if (!json) return [];
  137. if (Array.isArray(json.panels) && json.panels.length) {
  138. return json.panels.map((panel) => copyPanelMeta(panel, json));
  139. }
  140. if (isFarmAggregateJson(json)) {
  141. return farmAggregateToPanels(json, options);
  142. }
  143. return [copyPanelMeta(json, json)];
  144. }
  145. function formatTurbineLabel(name, nameMap = {}) {
  146. const raw = String(name || "");
  147. if (!raw) return "--";
  148. const mapped = nameMap[raw] || nameMap[String(raw)];
  149. const text = mapped ? String(mapped) : raw;
  150. if (/号$/.test(text)) return text;
  151. if (/^\d+$/.test(text)) return `${text}号`;
  152. return text;
  153. }
  154. function meanOf(values = []) {
  155. const nums = values.map(Number).filter((n) => Number.isFinite(n));
  156. if (!nums.length) return null;
  157. return nums.reduce((sum, n) => sum + n, 0) / nums.length;
  158. }
  159. function farmSeriesMode(detector, item = {}) {
  160. if (item.xType === "time") return "markers";
  161. if (detector === "wind_power_curve") return "lines";
  162. if (detector === "pitch_regulation") return "lines+markers";
  163. return "markers";
  164. }
  165. function farmSeriesKind(item = {}) {
  166. if (item.status) return String(item.status);
  167. if (item.anomaly) return "异常";
  168. return "正常";
  169. }
  170. function sliceByFlag(item, flags, keepAnomaly) {
  171. const xData = [];
  172. const yData = [];
  173. const timeData = [];
  174. const sourceTime = Array.isArray(item.timeData) ? item.timeData : [];
  175. (item.xData || []).forEach((x, idx) => {
  176. const flagged = Boolean(flags[idx]);
  177. if (flagged !== keepAnomaly) return;
  178. xData.push(x);
  179. yData.push((item.yData || [])[idx]);
  180. if (sourceTime[idx] != null) timeData.push(sourceTime[idx]);
  181. });
  182. return { xData, yData, timeData };
  183. }
  184. function buildFarmSeriesRows(item, detector, nameMap) {
  185. const turbineName = formatTurbineLabel(item.turbine, nameMap);
  186. const mode = farmSeriesMode(detector, item);
  187. const flags = Array.isArray(item.pointAnomaly) ? item.pointAnomaly : [];
  188. const canSplit =
  189. flags.length && flags.length === (item.xData || []).length;
  190. const rows = canSplit
  191. ? [
  192. { kind: "正常", sliced: sliceByFlag(item, flags, false) },
  193. { kind: "异常", sliced: sliceByFlag(item, flags, true) },
  194. ].filter((row) => row.sliced.xData.length)
  195. : [{ kind: farmSeriesKind(item), sliced: null }];
  196. return rows.map((row) => {
  197. const sliced = row.sliced;
  198. return {
  199. label: `${turbineName} ${row.kind}`,
  200. originalLabel: row.kind,
  201. mode,
  202. xData: sliced ? sliced.xData : item.xData || [],
  203. yData: sliced ? sliced.yData : item.yData || [],
  204. timeData: sliced
  205. ? sliced.timeData
  206. : item.xType === "time"
  207. ? item.xData || []
  208. : [],
  209. pointAnomaly: canSplit ? undefined : item.pointAnomaly,
  210. __farm: true,
  211. anomaly: Boolean(item.anomaly),
  212. extra: item,
  213. };
  214. });
  215. }
  216. export function farmAggregateToPanels(farmJson = {}, options = {}) {
  217. const detector = farmJson.detector || options.templateKey || "";
  218. const turbines = Array.isArray(farmJson.turbines) ? farmJson.turbines : [];
  219. const nameMap = options.nameMap || {};
  220. const title =
  221. options.title ||
  222. FARM_VALUE_TITLES[detector] ||
  223. FARM_SERIES_TITLES[detector] ||
  224. farmJson.farm ||
  225. "全场检测汇总";
  226. if (farmJson.chartType === "value") {
  227. const values = turbines.map((item) => Number(item.value));
  228. return [
  229. {
  230. chartKind: "valueBar",
  231. title,
  232. detectorName: detector,
  233. xaxis: "风机",
  234. yaxis: FARM_VALUE_AXIS[detector] || "检测值",
  235. categories: turbines.map((item) =>
  236. formatTurbineLabel(item.turbine, nameMap),
  237. ),
  238. values,
  239. anomalyFlags: turbines.map((item) => Boolean(item.anomaly)),
  240. extras: turbines,
  241. mean: meanOf(values),
  242. },
  243. ];
  244. }
  245. return [
  246. {
  247. title,
  248. xaxis: farmJson.xaxis || "",
  249. yaxis: farmJson.yaxis || "",
  250. xrange: farmJson.xrange,
  251. yrange: farmJson.yrange,
  252. detectorName: detector,
  253. data: turbines.flatMap((item, turbineIndex) =>
  254. buildFarmSeriesRows(item, detector, nameMap).map((row) => ({
  255. ...row,
  256. turbineIndex,
  257. engineName: formatTurbineLabel(item.turbine, nameMap),
  258. })),
  259. ),
  260. },
  261. ];
  262. }
  263. export function formatPointHover(point) {
  264. if (!point || typeof point !== "object") return "";
  265. const lines = [];
  266. if (point.dataTime) lines.push(`时间: ${point.dataTime}`);
  267. const appendMap = (obj, prefix) => {
  268. if (!obj || typeof obj !== "object") return;
  269. Object.keys(obj).forEach((key) => {
  270. const val = obj[key];
  271. if (val == null || val === "") return;
  272. lines.push(`${prefix}${key}: ${val}`);
  273. });
  274. };
  275. appendMap(point.raw, "");
  276. appendMap(point.deviation, "偏差 ");
  277. appendMap(point.mean, "均值 ");
  278. appendMap(point.meanDeviation, "均值偏差 ");
  279. return lines.join("<br/>");
  280. }
  281. export function formatFarmSeriesHover(extra = {}, detector = "") {
  282. if (!extra || typeof extra !== "object") return "";
  283. const lines = [];
  284. const push = (label, val, suffix = "") => {
  285. if (val == null || val === "") return;
  286. lines.push(`${label}: ${val}${suffix}`);
  287. };
  288. push("状态", extra.status);
  289. push("发电量比值", extra.ratio);
  290. push("异常点数", extra.anomalyPoints);
  291. push("降载占比", extra.deloadRatio);
  292. if (detector === "yaw_error") {
  293. push("误差", extra.value, "°");
  294. push("发电量损失", extra.powerloss, "%");
  295. push("覆盖率", extra.coverage);
  296. }
  297. if (detector === "yaw_count") {
  298. push("CW", extra.cw);
  299. push("CCW", extra.ccw);
  300. push("覆盖率", extra.coverage);
  301. }
  302. if (detector === "pitch_min") {
  303. push("P10", extra.p10, "°");
  304. push("基线", extra.baseline, "°");
  305. }
  306. return lines.join("<br/>");
  307. }
  308. export function formatFarmValueHover(extra = {}, detector = "") {
  309. if (!extra || typeof extra !== "object") return "";
  310. const lines = [];
  311. const push = (label, val, suffix = "") => {
  312. if (val == null || val === "") return;
  313. lines.push(`${label}: ${val}${suffix}`);
  314. };
  315. if (detector === "yaw_error") {
  316. push("误差", extra.value, "°");
  317. push("发电量损失", extra.powerloss, "%");
  318. push("覆盖率", extra.coverage != null ? Number(extra.coverage) : extra.coverage);
  319. } else if (detector === "yaw_count") {
  320. push("次数", extra.value);
  321. push("CW", extra.cw);
  322. push("CCW", extra.ccw);
  323. push("覆盖率", extra.coverage);
  324. } else if (detector === "pitch_min") {
  325. push("漂移", extra.value, "°");
  326. push("P10", extra.p10, "°");
  327. push("基线", extra.baseline, "°");
  328. } else {
  329. push("数值", extra.value);
  330. }
  331. return lines.join("<br/>");
  332. }
  333. export function scaleBubbleSize(value, min, max, { minSize = 8, maxSize = 28 } = {}) {
  334. const num = Number(value);
  335. if (!Number.isFinite(num)) return minSize;
  336. if (!Number.isFinite(min) || !Number.isFinite(max) || max <= min) return 14;
  337. const ratio = (num - min) / (max - min);
  338. return minSize + ratio * (maxSize - minSize);
  339. }