anomalyDiagnosticVisual.mjs 6.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221
  1. /**
  2. * 异常检测图表视觉语义(前端全场图 + 报告共用)
  3. *
  4. * 原则:
  5. * - 颜色优先表达诊断结论,不用彩虹色表达大量风机编号
  6. * - 异常同时使用红色、菱形和更高层级,避免只依赖颜色
  7. * - 连续变量使用感知均匀、色盲友好的 Viridis 色带
  8. */
  9. export const DIAGNOSTIC_COLORS = {
  10. normal: "#2E90FA",
  11. anomaly: "#F04438",
  12. warning: "#F79009",
  13. reference: "#98A2B3",
  14. mean: "#7F56D9",
  15. blade1: "#2E90FA",
  16. blade2: "#12B76A",
  17. blade3: "#7F56D9",
  18. };
  19. export const DIAGNOSTIC_CATEGORICAL_COLORS = [
  20. "#2E90FA",
  21. "#12B76A",
  22. "#F79009",
  23. "#7F56D9",
  24. "#06AED4",
  25. "#EE46BC",
  26. "#6172F3",
  27. "#47CD89",
  28. "#FDB022",
  29. "#84ADFF",
  30. ];
  31. /** 全场总图:正常风机冷色,异常风机红橙暖色;同组内用不同色相区分机组 */
  32. export const FARM_NORMAL_COOL_COLORS = Object.freeze([
  33. "#3B82F6",
  34. "#06B6D4",
  35. "#0EA5E9",
  36. "#2563EB",
  37. "#14B8A6",
  38. "#6366F1",
  39. "#38BDF8",
  40. "#0D9488",
  41. ]);
  42. export const FARM_ANOMALY_WARM_COLORS = Object.freeze([
  43. "#F04438",
  44. "#F97316",
  45. "#EF4444",
  46. "#FB7185",
  47. "#EA580C",
  48. "#E11D48",
  49. "#F59E0B",
  50. "#DC2626",
  51. ]);
  52. export const DIAGNOSTIC_CONTINUOUS_COLORS = [
  53. "#440154",
  54. "#3B528B",
  55. "#21918C",
  56. "#5EC962",
  57. "#FDE725",
  58. ];
  59. export const DIAGNOSTIC_FARM_STYLES = Object.freeze({
  60. normal: Object.freeze({
  61. pointOpacity: 0.5,
  62. lineWidth: 1,
  63. lineOpacity: 0.5,
  64. z: 1,
  65. }),
  66. anomaly: Object.freeze({
  67. pointOpacity: 1,
  68. lineWidth: 2.4,
  69. lineOpacity: 1,
  70. z: 20,
  71. }),
  72. category: Object.freeze({
  73. pointOpacity: 0.88,
  74. lineWidth: 1.8,
  75. lineOpacity: 1,
  76. z: 2,
  77. }),
  78. });
  79. function seriesText(rowOrLabel) {
  80. if (rowOrLabel && typeof rowOrLabel === "object") {
  81. return `${rowOrLabel.originalLabel || ""} ${rowOrLabel.label || ""}`;
  82. }
  83. return String(rowOrLabel || "");
  84. }
  85. export function classifyDiagnosticSeries(rowOrLabel) {
  86. const text = seriesText(rowOrLabel);
  87. if (/桨叶\s*1/.test(text)) return "blade1";
  88. if (/桨叶\s*2/.test(text)) return "blade2";
  89. if (/桨叶\s*3/.test(text)) return "blade3";
  90. if (/异常/.test(text) && !/正常/.test(text)) return "anomaly";
  91. if (/上限|下限|阈值|警戒|频繁/.test(text)) return "warning";
  92. if (/均值|平均|趋势/.test(text)) return "mean";
  93. if (/参考|理论|合同|基准|标准/.test(text) || rowOrLabel?.__shared) {
  94. return "reference";
  95. }
  96. if (/正常/.test(text)) return "normal";
  97. return "category";
  98. }
  99. export function diagnosticSeriesColor(rowOrLabel, index = 0) {
  100. const role = classifyDiagnosticSeries(rowOrLabel);
  101. if (DIAGNOSTIC_COLORS[role]) return DIAGNOSTIC_COLORS[role];
  102. const safeIndex = Math.abs(Number(index) || 0);
  103. return DIAGNOSTIC_CATEGORICAL_COLORS[
  104. safeIndex % DIAGNOSTIC_CATEGORICAL_COLORS.length
  105. ];
  106. }
  107. function paletteIndexOf(rowOrLabel, index = 0) {
  108. const turbineIndex = Number(rowOrLabel?.turbineIndex);
  109. if (Number.isFinite(turbineIndex)) return Math.abs(turbineIndex);
  110. return Math.abs(Number(index) || 0);
  111. }
  112. function turbineAnomalyFlag(rowOrLabel) {
  113. if (!rowOrLabel || typeof rowOrLabel !== "object") return null;
  114. if (typeof rowOrLabel.extra?.anomaly === "boolean") return rowOrLabel.extra.anomaly;
  115. if (typeof rowOrLabel.anomaly === "boolean") return rowOrLabel.anomaly;
  116. return null;
  117. }
  118. /**
  119. * 全场总图是否按「异常风机」上色。
  120. * 降载图正常/异常是同一台机的点,颜色仍跟风机走,点形再区分状态。
  121. */
  122. export function isFarmAnomalyTurbine(rowOrLabel, templateKey = "") {
  123. const flagged = turbineAnomalyFlag(rowOrLabel);
  124. if (typeof flagged === "boolean") return flagged;
  125. if (templateKey === "ctrl_deload") return false;
  126. return classifyDiagnosticSeries(rowOrLabel) === "anomaly";
  127. }
  128. export function farmStatusPaletteColor(
  129. rowOrLabel,
  130. index = 0,
  131. templateKey = "",
  132. ) {
  133. const palette = isFarmAnomalyTurbine(rowOrLabel, templateKey)
  134. ? FARM_ANOMALY_WARM_COLORS
  135. : FARM_NORMAL_COOL_COLORS;
  136. return palette[paletteIndexOf(rowOrLabel, index) % palette.length];
  137. }
  138. /**
  139. * 全场图颜色语义:
  140. * - 正常风机冷色、异常风机红橙暖色,组内再按机组区分;
  141. * - 降载图颜色跟风机,点形区分正常/异常点;
  142. * - 参考 / 阈值 / 均值保持语义色。
  143. */
  144. export function diagnosticFarmSeriesColor(
  145. rowOrLabel,
  146. index = 0,
  147. templateKey = "",
  148. ) {
  149. const role = classifyDiagnosticSeries(rowOrLabel);
  150. if (role === "reference" || role === "warning" || role === "mean") {
  151. return diagnosticSeriesColor(rowOrLabel, index);
  152. }
  153. return farmStatusPaletteColor(rowOrLabel, index, templateKey);
  154. }
  155. export function diagnosticFarmSymbolSize(
  156. rowOrLabel,
  157. pointCount = 0,
  158. templateKey = "",
  159. ) {
  160. const role = classifyDiagnosticSeries(rowOrLabel);
  161. if (role === "anomaly") return 14;
  162. if (role === "normal") return 7;
  163. if (templateKey === "pitch_coord") return pointCount > 800 ? 12 : 14;
  164. return pointCount > 500 ? 10 : 12;
  165. }
  166. export function diagnosticFarmStyle(rowOrLabel) {
  167. const role = classifyDiagnosticSeries(rowOrLabel);
  168. return (
  169. DIAGNOSTIC_FARM_STYLES[role] ||
  170. DIAGNOSTIC_FARM_STYLES.category
  171. );
  172. }
  173. export function diagnosticBladeStyle(rowOrLabel) {
  174. const role = classifyDiagnosticSeries(rowOrLabel);
  175. if (role === "blade1") {
  176. return { symbol: "rect", lineType: "dashed", showSymbol: true };
  177. }
  178. if (role === "blade2") {
  179. return { symbol: "circle", lineType: "solid", showSymbol: true };
  180. }
  181. if (role === "blade3") {
  182. return { symbol: "triangle", lineType: "dotted", showSymbol: true };
  183. }
  184. return null;
  185. }
  186. export function diagnosticSeriesSymbol(rowOrLabel, fallback = "circle") {
  187. const role = classifyDiagnosticSeries(rowOrLabel);
  188. if (role === "anomaly") return "diamond";
  189. if (role === "blade1") return "rect";
  190. if (role === "blade2") return "circle";
  191. if (role === "blade3") return "triangle";
  192. return fallback;
  193. }
  194. export function diagnosticLineType(rowOrLabel) {
  195. const role = classifyDiagnosticSeries(rowOrLabel);
  196. if (role === "warning" || role === "reference" || role === "mean") {
  197. return "dashed";
  198. }
  199. if (role === "blade1") return "dashed";
  200. if (role === "blade3") return "dotted";
  201. return "solid";
  202. }