anomalyFarmChartRules.mjs 7.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258
  1. /**
  2. * 全场异常检测图表的报告模板规则。
  3. * 前端页面和报告服务必须共同引用,避免面板数量与标题漂移。
  4. */
  5. export const FARM_DETECTOR_PANEL_RULES = Object.freeze({
  6. pitch_regulation: Object.freeze({
  7. count: 2,
  8. titles: Object.freeze([
  9. "各桨叶分风速段中位数趋势",
  10. "桨叶极差时序及异常识别",
  11. ]),
  12. }),
  13. pitch_coord: Object.freeze({
  14. count: 2,
  15. titles: Object.freeze([
  16. "功率-转速散点(桨距角着色)",
  17. "桨距角-转速散点",
  18. ]),
  19. }),
  20. ctrl_deload: Object.freeze({
  21. count: 1,
  22. titles: Object.freeze(["有功功率时序"]),
  23. }),
  24. ctrl_power_quality: Object.freeze({
  25. count: 3,
  26. titles: Object.freeze([
  27. "三相电流不平衡度",
  28. "三相电压不平衡度",
  29. "功率因数",
  30. ]),
  31. }),
  32. ctrl_op_state: Object.freeze({
  33. count: 1,
  34. titles: Object.freeze(["转速-功率散点"]),
  35. }),
  36. });
  37. export function getFarmPanelRule(templateKey) {
  38. return (
  39. FARM_DETECTOR_PANEL_RULES[templateKey] ||
  40. Object.freeze({ count: 1, titles: Object.freeze([]) })
  41. );
  42. }
  43. export function usesStitchedFarmPanels(templateKey) {
  44. return Boolean(FARM_DETECTOR_PANEL_RULES[templateKey]);
  45. }
  46. /** 与算法 farm_{detector}.json / 前端总图 Tab 一致的图种 */
  47. export const FARM_VALUE_DETECTORS = new Set([
  48. "yaw_error",
  49. "yaw_count",
  50. "pitch_min",
  51. ]);
  52. /**
  53. * 算法 farm JSON 只有 1 条叠加时,对应规则里的哪一面。
  54. * 其余分面由单机 JSON 拼接或页面占位补齐。
  55. */
  56. export const FARM_PAGE_PANEL_HINTS = Object.freeze({
  57. pitch_regulation: "极差",
  58. pitch_coord: "桨距",
  59. ctrl_deload: "有功功率",
  60. ctrl_op_state: "转速",
  61. ctrl_power_quality: "功率因数",
  62. });
  63. /** 这些检测器全场图用颜色区分风机,图例保留全部系列 */
  64. export const FARM_TURBINE_COLOR_DETECTORS = new Set([
  65. "pitch_regulation",
  66. "pitch_coord",
  67. "ctrl_deload",
  68. "ctrl_power_quality",
  69. "ctrl_op_state",
  70. ]);
  71. export function shouldKeepFullFarmLegend(templateKey) {
  72. return FARM_TURBINE_COLOR_DETECTORS.has(templateKey);
  73. }
  74. export function shouldUseFarmTurbineColor(templateKey) {
  75. return FARM_TURBINE_COLOR_DETECTORS.has(templateKey);
  76. }
  77. export function shouldExcludeFarmLabel(label, templateKey) {
  78. const text = String(label || "");
  79. if (templateKey === "wind_power_scatter") return /上限|下限/.test(text);
  80. if (templateKey === "yaw_twist") return /平均|均值/.test(text);
  81. if (templateKey === "yaw_static") {
  82. return /(?:2|12)\s*(?:h|H|小时)?\s*均值/.test(text);
  83. }
  84. if (templateKey === "pitch_regulation") return /阈值/.test(text);
  85. return false;
  86. }
  87. function panelText(panel) {
  88. return `${panel?.title || ""} ${panel?.xaxis || ""} ${panel?.yaxis || ""}`;
  89. }
  90. function panelLooksTimed(panel) {
  91. if (/时间|时序/.test(panelText(panel))) return true;
  92. const row = panel?.data?.[0];
  93. const sample =
  94. (Array.isArray(row?.timeData) && row.timeData[0]) ||
  95. (Array.isArray(row?.xData) && row.xData[0]) ||
  96. (Array.isArray(row?.x) && row.x[0]);
  97. if (typeof sample === "string" && /\d{4}-\d{2}-\d{2}/.test(sample)) return true;
  98. const n = Number(sample);
  99. return Number.isFinite(n) && n > 1e12;
  100. }
  101. export function matchFarmPanelSlot(panel, templateKey) {
  102. const text = panelText(panel);
  103. if (templateKey === "pitch_regulation") {
  104. if (/中位数|分风速段/.test(text) && !/极差/.test(text)) return 0;
  105. if (/极差/.test(text)) return 1;
  106. return 1;
  107. }
  108. if (templateKey === "pitch_coord") {
  109. if (/着色/.test(text) || (/功率/.test(text) && /转速/.test(text))) return 0;
  110. if (/桨距/.test(text)) return 1;
  111. return 1;
  112. }
  113. if (templateKey === "ctrl_power_quality") {
  114. if (/电流不平衡/.test(text)) return 0;
  115. if (/电压不平衡/.test(text)) return 1;
  116. if (/功率因数/.test(text)) return 2;
  117. return 2;
  118. }
  119. return 0;
  120. }
  121. export function selectFarmPanels(panels, templateKey) {
  122. const list = Array.isArray(panels) ? panels : [];
  123. const panelLimit = getFarmPanelRule(templateKey).count;
  124. if (templateKey === "ctrl_deload") {
  125. const power = list.filter((panel) => /有功功率/.test(panelText(panel)));
  126. const timed = power.filter((panel) => panelLooksTimed(panel));
  127. return (timed.length ? timed : power.length ? power : list).slice(
  128. 0,
  129. panelLimit,
  130. );
  131. }
  132. if (templateKey === "ctrl_op_state") {
  133. const matched = list.filter(
  134. (panel) => /转速/.test(panelText(panel)) && /功率/.test(panelText(panel)),
  135. );
  136. return (matched.length ? matched : list).slice(0, panelLimit);
  137. }
  138. if (templateKey === "pitch_regulation") {
  139. const medianTrend = list.find((panel) => {
  140. const text = panelText(panel);
  141. return /桨叶|中位数|分风速段/.test(text) && !/极差/.test(text);
  142. });
  143. const rangeTimeline = list.find((panel) => /极差/.test(panelText(panel)));
  144. const picked = [medianTrend, rangeTimeline].filter(Boolean);
  145. list.forEach((panel) => {
  146. if (picked.length < panelLimit && !picked.includes(panel)) {
  147. picked.push(panel);
  148. }
  149. });
  150. return picked.slice(0, panelLimit);
  151. }
  152. if (templateKey === "pitch_coord") {
  153. const powerSpeed = list.filter(
  154. (panel) => /转速/.test(panelText(panel)) && /功率/.test(panelText(panel)),
  155. );
  156. const pitchSpeed = list.filter((panel) => {
  157. const text = panelText(panel);
  158. return (
  159. /转速/.test(text) &&
  160. /桨距/.test(text) &&
  161. !/功率/.test(String(panel.yaxis || ""))
  162. );
  163. });
  164. const first =
  165. powerSpeed.find((panel) => /着色/.test(panel.title || "")) ||
  166. powerSpeed[0];
  167. const second = pitchSpeed.find((panel) => panel !== first);
  168. const picked = [first, second].filter(Boolean);
  169. if (picked.length >= panelLimit) return picked.slice(0, panelLimit);
  170. if (picked.length === 1) {
  171. const rest = list.find((panel) => panel !== picked[0]);
  172. return rest ? [picked[0], rest] : picked;
  173. }
  174. return list.slice(0, panelLimit);
  175. }
  176. if (templateKey === "ctrl_power_quality") {
  177. const picked = [];
  178. const seen = new Set();
  179. const pushUnique = (panel) => {
  180. const key = `${panel.yaxis || ""}|${panel.title || ""}`;
  181. if (seen.has(key)) return;
  182. seen.add(key);
  183. picked.push(panel);
  184. };
  185. list
  186. .filter((panel) => /电流不平衡/.test(panelText(panel)))
  187. .forEach(pushUnique);
  188. list
  189. .filter((panel) => /电压不平衡/.test(panelText(panel)))
  190. .forEach(pushUnique);
  191. list
  192. .filter((panel) => /功率因数/.test(panelText(panel)))
  193. .forEach(pushUnique);
  194. list.forEach((panel) => {
  195. if (picked.length < panelLimit && !picked.includes(panel)) {
  196. pushUnique(panel);
  197. }
  198. });
  199. return picked.slice(0, panelLimit);
  200. }
  201. return list.slice(0, panelLimit);
  202. }
  203. export function applyFarmPanelTitles(panels, templateKey) {
  204. const titles = getFarmPanelRule(templateKey).titles || [];
  205. if (!titles.length) return Array.isArray(panels) ? panels : [];
  206. const list = Array.isArray(panels) ? panels : [];
  207. if (list.length >= titles.length) {
  208. return list.map((panel, index) => ({
  209. ...panel,
  210. title: titles[index] || panel.title,
  211. }));
  212. }
  213. return list.map((panel) => {
  214. const slot = matchFarmPanelSlot(panel, templateKey);
  215. return {
  216. ...panel,
  217. title: titles[slot] || panel.title,
  218. };
  219. });
  220. }
  221. export function fillFarmPanelsToRule(realPanels, fillerPanels, templateKey) {
  222. const rule = getFarmPanelRule(templateKey);
  223. const count = Math.max(rule.count, 1);
  224. const titles = rule.titles || [];
  225. const slots = Array.from({ length: count }, (_, index) => {
  226. const filler = fillerPanels?.[index];
  227. if (!filler) return null;
  228. return {
  229. ...filler,
  230. title: titles[index] || filler.title,
  231. };
  232. });
  233. (realPanels || []).forEach((panel) => {
  234. const slot = Math.min(
  235. Math.max(matchFarmPanelSlot(panel, templateKey), 0),
  236. count - 1,
  237. );
  238. slots[slot] = {
  239. ...panel,
  240. title: titles[slot] || panel.title,
  241. };
  242. });
  243. return slots.filter(Boolean);
  244. }