| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310 |
- /**
- * 全场异常检测图表的报告模板规则。
- * 前端页面和报告服务必须共同引用,避免面板数量与标题漂移。
- */
- export const FARM_DETECTOR_PANEL_RULES = Object.freeze({
- pitch_regulation: Object.freeze({
- count: 2,
- titles: Object.freeze([
- "各桨叶分风速段中位数趋势",
- "桨叶极差时序及异常识别",
- ]),
- }),
- pitch_coord: Object.freeze({
- count: 2,
- titles: Object.freeze([
- "功率-转速散点(桨距角着色)",
- "桨距角-转速散点",
- ]),
- }),
- ctrl_deload: Object.freeze({
- count: 1,
- titles: Object.freeze(["有功功率时序"]),
- }),
- ctrl_power_quality: Object.freeze({
- count: 3,
- titles: Object.freeze([
- "三相电流不平衡度",
- "三相电压不平衡度",
- "功率因数",
- ]),
- }),
- ctrl_op_state: Object.freeze({
- count: 1,
- titles: Object.freeze(["转速-功率散点"]),
- }),
- });
- export function getFarmPanelRule(templateKey) {
- return (
- FARM_DETECTOR_PANEL_RULES[templateKey] ||
- Object.freeze({ count: 1, titles: Object.freeze([]) })
- );
- }
- export function usesStitchedFarmPanels(templateKey) {
- return Boolean(FARM_DETECTOR_PANEL_RULES[templateKey]);
- }
- /** 与算法 farm_{detector}.json / 前端总图 Tab 一致的图种 */
- export const FARM_VALUE_DETECTORS = new Set([
- "yaw_error",
- "yaw_count",
- "pitch_min",
- ]);
- /**
- * 算法 farm JSON 只有 1 条叠加时,对应规则里的哪一面。
- * 其余分面由单机 JSON 拼接或页面占位补齐。
- */
- export const FARM_PAGE_PANEL_HINTS = Object.freeze({
- pitch_regulation: "极差",
- pitch_coord: "桨距",
- ctrl_deload: "有功功率",
- ctrl_op_state: "转速",
- ctrl_power_quality: "功率因数",
- });
- /** 这些检测器全场图用颜色区分风机,图例保留全部系列 */
- export const FARM_TURBINE_COLOR_DETECTORS = new Set([
- "pitch_regulation",
- "pitch_coord",
- "ctrl_deload",
- "ctrl_power_quality",
- "ctrl_op_state",
- ]);
- export function shouldKeepFullFarmLegend(templateKey) {
- return FARM_TURBINE_COLOR_DETECTORS.has(templateKey);
- }
- export function shouldUseFarmTurbineColor(templateKey) {
- return FARM_TURBINE_COLOR_DETECTORS.has(templateKey);
- }
- export function shouldExcludeFarmLabel(label, templateKey) {
- const text = String(label || "");
- if (templateKey === "wind_power_scatter") return /上限|下限/.test(text);
- if (templateKey === "yaw_twist") return /平均|均值/.test(text);
- if (templateKey === "yaw_static") {
- return /(?:2|12)\s*(?:h|H|小时)?\s*均值/.test(text);
- }
- if (templateKey === "pitch_regulation") return /阈值/.test(text);
- return false;
- }
- function panelText(panel) {
- const key = panel?.panelKey || panel?.key || "";
- return `${key} ${panel?.title || ""} ${panel?.xaxis || ""} ${
- panel?.yaxis || ""
- }`;
- }
- function panelLooksTimed(panel) {
- if (/时间|时序/.test(panelText(panel))) return true;
- const row = panel?.data?.[0];
- const sample =
- (Array.isArray(row?.timeData) && row.timeData[0]) ||
- (Array.isArray(row?.xData) && row.xData[0]) ||
- (Array.isArray(row?.x) && row.x[0]);
- if (typeof sample === "string" && /\d{4}-\d{2}-\d{2}/.test(sample)) return true;
- const n = Number(sample);
- return Number.isFinite(n) && n > 1e12;
- }
- /** 电能质量三分面:兼容中文标题与算法 panels[].key */
- const POWER_QUALITY_SLOT_PATTERNS = [
- /电流不平衡|current_imbalance/,
- /电压不平衡|voltage_imbalance/,
- /功率因数|power_factor/,
- ];
- export function matchFarmPanelSlot(panel, templateKey) {
- const text = panelText(panel);
- if (templateKey === "pitch_regulation") {
- if (/中位数|分风速段/.test(text) && !/极差/.test(text)) return 0;
- if (/极差/.test(text)) return 1;
- return 1;
- }
- if (templateKey === "pitch_coord") {
- if (/着色/.test(text) || (/功率/.test(text) && /转速/.test(text))) return 0;
- if (/桨距/.test(text)) return 1;
- return 1;
- }
- if (templateKey === "ctrl_power_quality") {
- if (POWER_QUALITY_SLOT_PATTERNS[0].test(text)) return 0;
- if (POWER_QUALITY_SLOT_PATTERNS[1].test(text)) return 1;
- if (POWER_QUALITY_SLOT_PATTERNS[2].test(text)) return 2;
- return 2;
- }
- return 0;
- }
- export function selectFarmPanels(panels, templateKey) {
- const list = Array.isArray(panels) ? panels : [];
- const panelLimit = getFarmPanelRule(templateKey).count;
- if (templateKey === "ctrl_deload") {
- const power = list.filter((panel) => /有功功率/.test(panelText(panel)));
- const timed = power.filter((panel) => panelLooksTimed(panel));
- return (timed.length ? timed : power.length ? power : list).slice(
- 0,
- panelLimit,
- );
- }
- if (templateKey === "ctrl_op_state") {
- const matched = list.filter(
- (panel) => /转速/.test(panelText(panel)) && /功率/.test(panelText(panel)),
- );
- return (matched.length ? matched : list).slice(0, panelLimit);
- }
- if (templateKey === "pitch_regulation") {
- const medianTrend = list.find((panel) => {
- const text = panelText(panel);
- return /桨叶|中位数|分风速段/.test(text) && !/极差/.test(text);
- });
- const rangeTimeline = list.find((panel) => /极差/.test(panelText(panel)));
- const picked = [medianTrend, rangeTimeline].filter(Boolean);
- list.forEach((panel) => {
- if (picked.length < panelLimit && !picked.includes(panel)) {
- picked.push(panel);
- }
- });
- return picked.slice(0, panelLimit);
- }
- if (templateKey === "pitch_coord") {
- const powerSpeed = list.filter(
- (panel) => /转速/.test(panelText(panel)) && /功率/.test(panelText(panel)),
- );
- const pitchSpeed = list.filter((panel) => {
- const text = panelText(panel);
- return (
- /转速/.test(text) &&
- /桨距/.test(text) &&
- !/功率/.test(String(panel.yaxis || ""))
- );
- });
- const first =
- powerSpeed.find((panel) => /着色/.test(panel.title || "")) ||
- powerSpeed[0];
- const second = pitchSpeed.find((panel) => panel !== first);
- const picked = [first, second].filter(Boolean);
- if (picked.length >= panelLimit) return picked.slice(0, panelLimit);
- if (picked.length === 1) {
- const rest = list.find((panel) => panel !== picked[0]);
- return rest ? [picked[0], rest] : picked;
- }
- return list.slice(0, panelLimit);
- }
- if (templateKey === "ctrl_power_quality") {
- const picked = [];
- const seen = new Set();
- const pushUnique = (panel) => {
- const key = `${panel.yaxis || ""}|${panel.title || ""}`;
- if (seen.has(key)) return;
- seen.add(key);
- picked.push(panel);
- };
- POWER_QUALITY_SLOT_PATTERNS.forEach((pattern) => {
- list.filter((panel) => pattern.test(panelText(panel))).forEach(pushUnique);
- });
- list.forEach((panel) => {
- if (picked.length < panelLimit && !picked.includes(panel)) {
- pushUnique(panel);
- }
- });
- return picked.slice(0, panelLimit);
- }
- return list.slice(0, panelLimit);
- }
- export function applyFarmPanelTitles(panels, templateKey) {
- const titles = getFarmPanelRule(templateKey).titles || [];
- if (!titles.length) return Array.isArray(panels) ? panels : [];
- const list = Array.isArray(panels) ? panels : [];
- if (list.length >= titles.length) {
- return list.map((panel, index) => ({
- ...panel,
- title: titles[index] || panel.title,
- }));
- }
- return list.map((panel) => {
- const slot = matchFarmPanelSlot(panel, templateKey);
- return {
- ...panel,
- title: titles[slot] || panel.title,
- };
- });
- }
- export function fillFarmPanelsToRule(realPanels, fillerPanels, templateKey) {
- const rule = getFarmPanelRule(templateKey);
- const count = Math.max(rule.count, 1);
- const titles = rule.titles || [];
- const list = Array.isArray(realPanels) ? realPanels : [];
- // JSON 已给全分面时按原顺序落位,避免标题相近时被匹配到同一格
- if (list.length >= count) {
- return applyFarmPanelTitles(list.slice(0, count), templateKey);
- }
- const slots = Array.from({ length: count }, (_, index) => {
- const filler = fillerPanels?.[index];
- if (!filler) return null;
- return {
- ...filler,
- title: titles[index] || filler.title,
- };
- });
- (realPanels || []).forEach((panel) => {
- const slot = Math.min(
- Math.max(matchFarmPanelSlot(panel, templateKey), 0),
- count - 1,
- );
- slots[slot] = {
- ...panel,
- title: titles[slot] || panel.title,
- };
- });
- return slots.filter(Boolean);
- }
- function readPointCoord(point) {
- if (Array.isArray(point)) return point;
- if (Array.isArray(point?.value)) return point.value;
- return null;
- }
- /** 系列数据在 x/y 上的最小值,供坐标轴从 0 起或向负数自适应 */
- export function seriesAxisDataMin(series = []) {
- let xMin = Infinity;
- let yMin = Infinity;
- (series || []).forEach((item) => {
- (item.data || []).forEach((point) => {
- const coord = readPointCoord(point);
- if (!coord) return;
- const x = Number(coord[0]);
- const y = Number(coord[1]);
- if (Number.isFinite(x) && x < xMin) xMin = x;
- if (Number.isFinite(y) && y < yMin) yMin = y;
- });
- });
- return {
- xMin: Number.isFinite(xMin) ? xMin : 0,
- yMin: Number.isFinite(yMin) ? yMin : 0,
- };
- }
- /**
- * 数据都非负时轴从 0 开始;出现负数时返回 undefined,不锁最小值。
- */
- export function axisMinStartingAtZero(dataMin) {
- const value = Number(dataMin);
- if (Number.isFinite(value) && value < 0) return undefined;
- return 0;
- }
- export function applyZeroStartAxis(axis, dataMin) {
- if (!axis) return axis;
- const min = axisMinStartingAtZero(dataMin);
- if (min === 0) axis.min = 0;
- else delete axis.min;
- return axis;
- }
|