| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731 |
- /**
- * 算法检测器 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),
- );
- }
- /** 多图检测器:顶层 chartType=panels,每个 panel 自带 turbines */
- export function isFarmPanelsJson(plotJson) {
- return Boolean(
- plotJson &&
- plotJson.chartType === "panels" &&
- Array.isArray(plotJson.panels) &&
- plotJson.panels.some((panel) => Array.isArray(panel?.turbines)),
- );
- }
- export function isFarmPlotJson(plotJson) {
- return isFarmAggregateJson(plotJson) || isFarmPanelsJson(plotJson);
- }
- 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 usesBubbleSymbolSize(row = {}, templateKey = "") {
- if (isPitchMinDetector(templateKey)) return false;
- return /点数/.test(String(row.colorbarTitle || row.colorbartitle || ""));
- }
- /**
- * 连续色映射取值:优先 colorbar,其次数值型 pointData(与 x/y 同下标)。
- * 点数色带即用该序列控制每个点的颜色深浅。
- */
- export function resolveRowColorMetric(row = {}, length = 0) {
- const len =
- Number(length) > 0
- ? Number(length)
- : Math.min(
- Array.isArray(row?.xData) ? row.xData.length : 0,
- Array.isArray(row?.yData) ? row.yData.length : 0,
- );
- if (!len) return [];
- const colorbar = row.colorbar || row.colorBar;
- if (Array.isArray(colorbar) && colorbar.length >= len) {
- return colorbar.slice(0, len).map((v) => {
- const n = Number(v);
- return Number.isFinite(n) ? n : 0;
- });
- }
- const pointData = row.pointData;
- if (!Array.isArray(pointData) || pointData.length < len) return [];
- // pointData 若为悬停对象数组则不可作色值
- if (pointData.some((item) => item != null && typeof item === "object")) {
- return [];
- }
- return pointData.slice(0, len).map((v) => {
- const n = Number(v);
- return Number.isFinite(n) ? n : 0;
- });
- }
- /** 点数大的点后绘制(盖在浅色点之上),并写入更高 z */
- export function sortScatterPairsByColorMetricAsc(pairs = []) {
- if (!Array.isArray(pairs) || pairs.length < 2) {
- return attachCountRenderZ(pairs);
- }
- const metricOf = (item) => {
- const value = Array.isArray(item) ? item : item?.value;
- if (!Array.isArray(value) || value.length < 3) return 0;
- const n = Number(value[2]);
- return Number.isFinite(n) ? n : 0;
- };
- return attachCountRenderZ(
- [...pairs].sort((a, b) => metricOf(a) - metricOf(b)),
- );
- }
- function attachCountRenderZ(pairs = []) {
- if (!Array.isArray(pairs) || !pairs.length) return pairs;
- return pairs.map((item) => {
- const value = Array.isArray(item) ? item : item?.value;
- const count =
- Array.isArray(value) && value.length >= 3 ? Number(value[2]) : 0;
- const z = Number.isFinite(count) ? Math.max(1, Math.round(count)) : 1;
- if (Array.isArray(item)) return { value: item, z };
- return { ...item, z };
- });
- }
- 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 };
- }
- /** 仅透传 JSON 中的小标题字段,不做前端拼接 */
- export function resolvePanelSubtitle(panel = {}, plotJson = {}) {
- const text =
- panel.subtitle ??
- panel.subtext ??
- panel.panelSubtitle ??
- plotJson.subtitle ??
- plotJson.subtext ??
- "";
- return String(text || "").trim();
- }
- function copyPanelMeta(panel, plotJson = {}) {
- return {
- panelKey: panel.key || "",
- title: panel.panelTitle || panel.title || plotJson.title || "",
- subtitle: resolvePanelSubtitle(panel, plotJson),
- xaxis: panel.xaxis || plotJson.xaxis || "",
- yaxis: panel.yaxis || plotJson.yaxis || "",
- xrange: panel.xrange || plotJson.xrange,
- yrange: panel.yrange || plotJson.yrange,
- data: panel.data || [],
- 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 (isFarmPlotJson(raw)) return raw;
- if (raw.data && isFarmPlotJson(raw.data)) return raw.data;
- return raw;
- }
- /** panels[] 里自带 turbines 的分面,按全场叠加图规则展开 */
- function farmPanelToPanel(panel = {}, farmJson = {}, options = {}) {
- const title = panel.title || panel.panelTitle || "";
- const [built] = farmAggregateToPanels(
- {
- detector: farmJson.detector || options.templateKey || "",
- farm: farmJson.farm,
- chartType: panel.chartType || "series",
- xaxis: panel.xaxis,
- yaxis: panel.yaxis,
- xrange: panel.xrange,
- yrange: panel.yrange,
- turbines: panel.turbines,
- referenceCurve: panel.referenceCurve || farmJson.referenceCurve,
- referenceCurves: panel.referenceCurves || farmJson.referenceCurves,
- contractCurve: panel.contractCurve || farmJson.contractCurve,
- },
- { ...options, title: title || options.title },
- );
- if (!built) return null;
- return {
- ...built,
- panelKey: panel.key || "",
- title: title || built.title,
- xaxis: panel.xaxis || built.xaxis,
- yaxis: panel.yaxis || built.yaxis,
- };
- }
- 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) =>
- Array.isArray(panel?.turbines)
- ? farmPanelToPanel(panel, json, options)
- : copyPanelMeta(panel, json),
- )
- .filter(Boolean);
- }
- 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 referenceCurveDisplayLabel(item = {}) {
- const text = String(item.label || "合同功率曲线");
- if (/合同功率/.test(text)) return "合同功率曲线";
- if (/近90日/.test(text)) return "近90日功率曲线";
- if (/参考功率|理论功率|参考曲线/.test(text)) return "参考功率曲线";
- return text;
- }
- function buildReferenceCurveRow(item) {
- if (!item || typeof item !== "object") return null;
- const xData = Array.isArray(item.xData)
- ? item.xData
- : Array.isArray(item.x)
- ? item.x
- : [];
- const yData = Array.isArray(item.yData)
- ? item.yData
- : Array.isArray(item.y)
- ? item.y
- : [];
- if (!xData.length) return null;
- const label = referenceCurveDisplayLabel(item);
- return {
- label,
- originalLabel: item.label || label,
- mode: "lines",
- xData,
- yData,
- timeData: Array.isArray(item.timeData) ? item.timeData : [],
- __shared: true,
- __farm: true,
- extra: item,
- };
- }
- function collectFarmReferenceRows(farmJson = {}) {
- const list = [];
- if (farmJson.referenceCurve) list.push(farmJson.referenceCurve);
- if (Array.isArray(farmJson.referenceCurves)) {
- list.push(...farmJson.referenceCurves);
- }
- if (farmJson.contractCurve) list.push(farmJson.contractCurve);
- const rows = list.map(buildReferenceCurveRow).filter(Boolean);
- const seen = new Set();
- return rows.filter((row) => {
- if (seen.has(row.label)) return false;
- seen.add(row.label);
- return true;
- });
- }
- /** pointAnomaly[i]===true 才是异常点;字符串 "false" 不能当成 true */
- export function isPointAnomalyFlag(flag) {
- return flag === true || flag === 1 || flag === "1" || flag === "true";
- }
- export function readPointAnomalyFlags(item) {
- if (!item || typeof item !== "object") return null;
- const candidates = [item.pointAnomaly, item.point_anomaly];
- if (!Array.isArray(item.pointAnomaly) && item.extra && item.extra !== item) {
- candidates.push(item.extra.pointAnomaly, item.extra.point_anomaly);
- }
- for (let i = 0; i < candidates.length; i += 1) {
- const raw = candidates[i];
- if (Array.isArray(raw)) return raw;
- if (typeof raw === "string" && raw.trim().startsWith("[")) {
- try {
- const parsed = JSON.parse(raw);
- if (Array.isArray(parsed)) return parsed;
- } catch (_error) {
- /* 忽略无法解析的标记 */
- }
- }
- }
- return null;
- }
- /**
- * 按 pointAnomaly 把一条折线拆成首尾相接的红/蓝段。
- * 相邻点只要有一端是异常,这段线就是红线,正常点之间是蓝线。
- */
- export function splitConnectedLineByPointAnomaly(xData = [], yData = [], flags) {
- const len = Math.min(xData.length, yData.length);
- const hasFlags = Array.isArray(flags) && flags.length > 0;
- const flagged = (index) => hasFlags && isPointAnomalyFlag(flags[index]);
- if (!len) return [];
- if (!hasFlags || len === 1) {
- return [
- {
- anomaly: flagged(0),
- xData: xData.slice(0, len),
- yData: yData.slice(0, len),
- },
- ];
- }
- const segments = [];
- let start = 0;
- let anomaly = flagged(0) || flagged(1);
- for (let index = 1; index < len - 1; index += 1) {
- const next = flagged(index) || flagged(index + 1);
- if (next === anomaly) continue;
- segments.push({
- anomaly,
- xData: xData.slice(start, index + 1),
- yData: yData.slice(start, index + 1),
- });
- start = index;
- anomaly = next;
- }
- segments.push({
- anomaly,
- xData: xData.slice(start, len),
- yData: yData.slice(start, len),
- });
- return segments.filter((segment) => segment.xData.length > 0);
- }
- function farmSeriesKind(item = {}, detector = "") {
- // 风功率散点没有逐点 pointAnomaly 时,不按整机 anomaly 把全部点画红
- if (detector === "wind_power_scatter") return "正常";
- if (item.status) return String(item.status);
- if (item.anomaly) return "异常";
- return "正常";
- }
- function sliceByFlag(item, flags, keepAnomaly) {
- const xData = [];
- const yData = [];
- const timeData = [];
- const sourceX = item.xData || [];
- const sourceY = item.yData || [];
- const sourceTime = Array.isArray(item.timeData) ? item.timeData : [];
- const len = Math.min(sourceX.length, sourceY.length);
- for (let idx = 0; idx < len; idx += 1) {
- // pointAnomaly[i] 与 xData/yData 同下标;缺省按正常(false)
- const flagged = isPointAnomalyFlag(flags[idx]);
- if (flagged !== keepAnomaly) continue;
- xData.push(sourceX[idx]);
- yData.push(sourceY[idx]);
- if (sourceTime[idx] != null) timeData.push(sourceTime[idx]);
- }
- return { xData, yData, timeData };
- }
- /** 变桨一致性等检测器:一台风机内按桨叶分多条曲线 */
- function buildFarmBladeRows(item, turbineName, mode) {
- const turbineAnomaly = Boolean(item.anomaly);
- return (item.blades || [])
- .filter((blade) => blade && Array.isArray(blade.xData))
- .map((blade) => {
- const bladeLabel = blade.label || "";
- return {
- label: `${turbineName} ${bladeLabel}`.trim(),
- originalLabel: bladeLabel,
- mode,
- xData: blade.xData || [],
- yData: blade.yData || [],
- timeData: Array.isArray(blade.timeData)
- ? blade.timeData
- : item.xType === "time"
- ? blade.xData || []
- : [],
- __farm: true,
- anomaly: turbineAnomaly,
- pointAnomaly: Array.isArray(blade.pointAnomaly)
- ? blade.pointAnomaly
- : Array.isArray(blade.point_anomaly)
- ? blade.point_anomaly
- : Array.isArray(item.pointAnomaly)
- ? item.pointAnomaly
- : Array.isArray(item.point_anomaly)
- ? item.point_anomaly
- : undefined,
- extra: {
- ...item,
- anomaly: turbineAnomaly,
- bladeLabel,
- },
- };
- });
- }
- function isJsonAnomalyFlag(flag) {
- return flag === true || flag === 1 || flag === "1" || flag === "true";
- }
- function buildFarmSeriesRows(item, detector, nameMap) {
- const turbineName = formatTurbineLabel(item.turbine, nameMap);
- const mode = farmSeriesMode(detector, item);
- // 功率曲线整机是否异常只看 JSON 的 anomaly,不用 status / pointAnomaly
- if (detector === "wind_power_curve") {
- const turbineAnomaly = isJsonAnomalyFlag(item.anomaly);
- const kind = turbineAnomaly ? "异常" : "正常";
- return [
- {
- label: `${turbineName} ${kind}`,
- originalLabel: kind,
- mode,
- xData: item.xData || [],
- yData: item.yData || [],
- timeData: Array.isArray(item.timeData) ? item.timeData : [],
- __farm: true,
- anomaly: turbineAnomaly,
- extra: { ...item, anomaly: turbineAnomaly },
- },
- ];
- }
- if (Array.isArray(item.blades) && item.blades.length) {
- return buildFarmBladeRows(item, turbineName, mode);
- }
- // 桨叶极差:保留整条折线和 pointAnomaly,由图表按段接成红蓝线
- if (detector === "pitch_regulation") {
- const pitchFlags = readPointAnomalyFlags(item);
- return [
- {
- label: turbineName,
- originalLabel: turbineName,
- mode,
- xData: item.xData || [],
- yData: item.yData || [],
- timeData: Array.isArray(item.timeData)
- ? item.timeData
- : item.xType === "time"
- ? item.xData || []
- : [],
- pointAnomaly: pitchFlags || undefined,
- __farm: true,
- anomaly: Boolean(item.anomaly),
- extra: {
- ...item,
- anomaly: Boolean(item.anomaly),
- turbineName,
- },
- },
- ];
- }
- const flags = readPointAnomalyFlags(item) || [];
- // 有 pointAnomaly 时按同下标拆分:false=正常点、true=异常点(不以整机 anomaly 覆盖)
- const canSplit = flags.length > 0;
- const rows = canSplit
- ? [
- { kind: "正常", sliced: sliceByFlag(item, flags, false) },
- { kind: "异常", sliced: sliceByFlag(item, flags, true) },
- ].filter((row) => row.sliced.xData.length)
- : [{ kind: farmSeriesKind(item, detector), sliced: null }];
- return rows.map((row) => {
- const sliced = row.sliced;
- const pointAnomalyFlag = canSplit
- ? row.kind === "异常"
- : Boolean(item.anomaly);
- 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: pointAnomalyFlag,
- // 行级 anomaly 写入 extra,避免着色仍读到整机 anomaly
- extra: { ...item, anomaly: pointAnomalyFlag },
- };
- });
- }
- 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));
- const categories = turbines.map((item) =>
- formatTurbineLabel(item.turbine, nameMap),
- );
- const valuePanel = {
- chartKind: "valueBar",
- title,
- detectorName: detector,
- xaxis: "风机",
- yaxis: FARM_VALUE_AXIS[detector] || "检测值",
- categories,
- values,
- anomalyFlags: turbines.map((item) => Boolean(item.anomaly)),
- extras: turbines,
- mean: meanOf(values),
- };
- if (detector === "yaw_count" || detector === "pitch_min") {
- return [{ ...valuePanel, colorMode: "farmStatus", showMean: false }];
- }
- if (detector !== "yaw_error") return [valuePanel];
- return [
- {
- ...valuePanel,
- title,
- xaxis: "机组编号",
- yaxis: "静态偏航误差(度)",
- values,
- colorMode: "yawErrorAbs",
- showMean: false,
- },
- ];
- }
- 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),
- })),
- )
- .concat(collectFarmReferenceRows(farmJson)),
- },
- ];
- }
- 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.deviations, "");
- appendMap(point.deviation, "偏差 ");
- appendMap(point.mean, "均值 ");
- appendMap(point.meanDeviation, "均值偏差 ");
- return lines.join("<br/>");
- }
- 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}`);
- };
- if (extra.bladeLabel) {
- const turbine =
- extra.turbineName != null ? String(extra.turbineName) : "";
- const turbineText = turbine
- ? /号$/.test(turbine)
- ? turbine
- : `${turbine}号`
- : "";
- lines.push([turbineText, extra.bladeLabel].filter(Boolean).join(" "));
- }
- push("状态", extra.status);
- push("发电量比值", extra.ratio);
- push("异常点数", extra.anomalyPoints);
- push("降载占比", extra.deloadRatio);
- if (detector === "yaw_error") {
- push("发电量损失", extra.powerloss, "%");
- push("覆盖率", extra.coverage);
- }
- if (detector === "yaw_count") {
- push("CW", extra.cw, "次");
- push("CCW", extra.ccw, "次");
- }
- if (detector === "pitch_min") {
- push("P10", extra.p10, "°");
- push("基线", extra.baseline, "°");
- }
- return lines.join("<br/>");
- }
- 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.powerloss, "%");
- push(
- "覆盖率",
- extra.coverage != null ? Number(extra.coverage) : extra.coverage,
- );
- } else if (detector === "yaw_count") {
- push("CW", extra.cw, "次");
- push("CCW", extra.ccw, "次");
- } else if (detector === "pitch_min") {
- push("漂移", extra.value, "°");
- push("P10", extra.p10, "°");
- push("基线", extra.baseline, "°");
- } else {
- push("数值", extra.value);
- }
- return lines.join("<br/>");
- }
- 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);
- }
- /** 异常检测报告数值轴刻度:最多保留 2 位小数 */
- export function formatReportValueAxisLabel(value, fractionDigits = 2) {
- const n = Number(value);
- if (!Number.isFinite(n)) return String(value ?? "");
- const rounded = Number(n.toFixed(fractionDigits));
- return rounded.toLocaleString("zh-CN", {
- minimumFractionDigits: 0,
- maximumFractionDigits: fractionDigits,
- });
- }
|