| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610 |
- /**
- * 风功率曲线 / 风功率散点 — 全场 3D 图表(前端 + 报告 Node 共用)
- * 展示坐标:X = 风机机位,Y = 风速,Z = 功率(竖直)
- * 原始 panel 数据仍为 xData=风速、yData=功率
- */
- import {
- classifyDiagnosticSeries,
- diagnosticSeriesColor,
- } from "./anomalyDiagnosticVisual.mjs";
- export const WIND_POWER_3D_TEMPLATE_KEYS = new Set([
- "wind_power_curve",
- "wind_power_scatter",
- ]);
- export const WIND_POWER_3D_AXIS = {
- x: "风机机位",
- y: "风速 (m/s)",
- z: "有功功率 (kW)",
- };
- /** 参考图视角:功率轴竖直,风机与风速轴分别向左、右前方展开 */
- const REFERENCE_VIEW_CONTROL = {
- alpha: 25,
- beta: 45,
- center: [0, -4, 0],
- };
- /** Y 轴(机组)最多展示的名称数(折线/散点数据仍全部绘制) */
- export const FARM_WIND_POWER_3D_TURBINE_LABEL_COUNT = 8;
- /** 机组在 Y 轴上的间距(数值越大,各风机切片越分开) */
- const TURBINE_Y_GAP = 10;
- /** 全场 3D 渐变色:提高饱和度与明度,避免深色底上看不清 */
- function farmTurbineGradientColor(turbineIndex, maxIndex, isAnomaly = false) {
- const t =
- maxIndex <= 1 ? 0 : Math.max(0, Math.min(1, (turbineIndex - 1) / (maxIndex - 1)));
- const hue = 88 + t * 172;
- if (isAnomaly) return `hsla(${hue}, 92%, 64%, 1)`;
- return `hsla(${hue}, 88%, 66%, 1)`;
- }
- function isAnomalyRow(row) {
- const text = `${row?.originalLabel || ""} ${row?.label || ""}`;
- return /异常/.test(text) && !/正常/.test(text);
- }
- function resolveFarmSeriesColor(row, index, turbineMeta, pickColor, farmMode) {
- if (!farmMode) return pickColor(row.label, index, row, farmMode);
- const role = classifyDiagnosticSeries(row);
- if (role === "reference" || role === "warning" || role === "mean") {
- return diagnosticSeriesColor(row, index);
- }
- if (typeof pickColor === "function") {
- return pickColor(row.label, index, row, farmMode);
- }
- const ti = resolveTurbineIndex(row, turbineMeta);
- return farmTurbineGradientColor(ti, turbineMeta.maxIndex, role === "anomaly");
- }
- function resolveFarmSeriesVisual(row, templateKey, turbineMax, reportMode) {
- const isRef = isReferenceLineRow(row);
- const isScatter = !isRef && isMarkerRow(row, templateKey);
- const isAnomaly = isAnomalyRow(row);
- const dense = turbineMax > 10;
- if (isRef) {
- return {
- lineWidth: 2.2,
- lineOpacity: 1,
- lineType: "dashed",
- itemOpacity: 1,
- symbolSize: 3,
- };
- }
- if (isScatter) {
- return {
- lineWidth: 1,
- lineOpacity: 1,
- lineType: "solid",
- itemOpacity: dense ? (isAnomaly ? 0.95 : 0.82) : 0.95,
- symbolSize: dense ? (isAnomaly ? 6.5 : 5.5) : 7,
- };
- }
- return {
- lineWidth: dense ? (reportMode ? 2 : 1.9) : 2.4,
- lineOpacity: isAnomaly ? 1 : dense ? 0.92 : 0.98,
- lineType: "solid",
- itemOpacity: isAnomaly ? 1 : 0.92,
- symbolSize: 3,
- };
- }
- function buildAxisSplitStyle(splitColor, show = true) {
- return {
- show,
- lineStyle: { color: splitColor, type: "solid", opacity: show ? 0.35 : 0 },
- };
- }
- export function isWindPower3DTemplate(templateKey) {
- return WIND_POWER_3D_TEMPLATE_KEYS.has(String(templateKey || ""));
- }
- /** 仅全场(farmMode)风功率曲线/散点使用 3D,单机保持 2D */
- export function shouldUseWindPower3D(templateKey, farmMode = false) {
- return Boolean(farmMode) && isWindPower3DTemplate(templateKey);
- }
- function getRowArrays(row) {
- const xData = Array.isArray(row?.xData) ? row.xData : [];
- const yData = Array.isArray(row?.yData) ? row.yData : [];
- const zData = Array.isArray(row?.zData) ? row.zData : [];
- return { xData, yData, zData };
- }
- function formatEngineLabel(name) {
- if (name == null || name === "") return "";
- const text = String(name);
- if (/号$/.test(text)) return text;
- if (/^\d+$/.test(text)) return `${text}号`;
- return text;
- }
- function resolveTurbineName(row) {
- if (row?.engineName) return formatEngineLabel(row.engineName);
- const label = String(row?.label || "");
- const head = label.split(/\s+/)[0] || "";
- if (/号/.test(head) || /^\d+$/.test(head)) return formatEngineLabel(head);
- return head;
- }
- function buildTurbineAxisMeta(rows = []) {
- const nameToIndex = new Map();
- let maxIndex = 0;
- rows.forEach((row) => {
- if (isReferenceLineRow(row)) return;
- const name = resolveTurbineName(row);
- let turbineIndex =
- row?.turbineIndex != null && Number.isFinite(Number(row.turbineIndex))
- ? Number(row.turbineIndex) + 1
- : null;
- if (turbineIndex == null && name) {
- if (!nameToIndex.has(name)) {
- maxIndex += 1;
- nameToIndex.set(name, maxIndex);
- }
- turbineIndex = nameToIndex.get(name);
- }
- if (turbineIndex != null) {
- maxIndex = Math.max(maxIndex, turbineIndex);
- if (name) nameToIndex.set(name, turbineIndex);
- }
- });
- const indexToName = new Map();
- nameToIndex.forEach((index, name) => indexToName.set(index, name));
- return { maxIndex, indexToName, nameToIndex };
- }
- function resolveTurbineIndex(row, meta) {
- if (row?.turbineIndex != null && Number.isFinite(Number(row.turbineIndex))) {
- return Number(row.turbineIndex) + 1;
- }
- const name = resolveTurbineName(row);
- if (name && meta.nameToIndex.has(name)) return meta.nameToIndex.get(name);
- return 1;
- }
- /** ECharts 3D 坐标:[机组位置, 风速, 功率] */
- function toChartPoint(windSpeed, power, turbineIndex) {
- return [turbineIndex * TURBINE_Y_GAP, windSpeed, power];
- }
- function resolveTurbineIndexFromPosition(position) {
- return Math.round(Number(position) / TURBINE_Y_GAP);
- }
- function resolveFarmTriplets(row, meta) {
- const { xData, yData, zData } = getRowArrays(row);
- const powerArr = zData.length ? zData : yData;
- const turbineIndex = resolveTurbineIndex(row, meta);
- const len = Math.min(xData.length, powerArr.length);
- const triplets = [];
- for (let i = 0; i < len; i += 1) {
- const windSpeed = Number(xData[i]);
- const power = Number(powerArr[i]);
- if (!Number.isFinite(windSpeed) || !Number.isFinite(power)) continue;
- triplets.push(toChartPoint(windSpeed, power, turbineIndex));
- }
- return triplets;
- }
- function isReferenceLineRow(row) {
- if (row?.__shared) return true;
- const text = `${row?.originalLabel || ""} ${row?.label || ""}`;
- return /参考功率|理论功率|合同功率|参考曲线/.test(text);
- }
- function isMarkerRow(row, templateKey) {
- if (isReferenceLineRow(row)) return false;
- if (templateKey === "wind_power_curve") return false;
- if (templateKey === "wind_power_scatter") return true;
- const mode = String(row?.mode || "lines");
- return mode.includes("markers") && !mode.includes("lines");
- }
- function buildReferenceLineTriplets(row) {
- const { xData, yData, zData } = getRowArrays(row);
- const powerArr = zData.length ? zData : yData;
- const len = Math.min(xData.length, powerArr.length);
- const triplets = [];
- for (let i = 0; i < len; i += 1) {
- const windSpeed = Number(xData[i]);
- const power = Number(powerArr[i]);
- if (!Number.isFinite(windSpeed) || !Number.isFinite(power)) continue;
- triplets.push(toChartPoint(windSpeed, power, 0));
- }
- return triplets;
- }
- function formatTurbineAxisLabel(index, indexToName) {
- if (index === 0) return "参考";
- return indexToName.get(index) || `${index}号`;
- }
- /** 机组较多时 Y 轴仅抽样展示名称,避免标签重叠 */
- function buildTurbineAxisLabelConfig(maxIndex, indexToName, hasReference) {
- const labeledIndex = new Set();
- if (hasReference) labeledIndex.add(0);
- if (maxIndex <= FARM_WIND_POWER_3D_TURBINE_LABEL_COUNT) {
- for (let index = 1; index <= maxIndex; index += 1) labeledIndex.add(index);
- } else {
- labeledIndex.add(1);
- labeledIndex.add(maxIndex);
- const slots = FARM_WIND_POWER_3D_TURBINE_LABEL_COUNT - labeledIndex.size;
- const step = Math.max(1, Math.round((maxIndex - 1) / (slots + 1)));
- for (let index = 1 + step; index < maxIndex; index += step) {
- labeledIndex.add(index);
- if (labeledIndex.size >= FARM_WIND_POWER_3D_TURBINE_LABEL_COUNT) break;
- }
- }
- const labelInterval = TURBINE_Y_GAP;
- return {
- interval: labelInterval,
- labeledIndex,
- formatter(v) {
- const position = Number(v);
- if (!Number.isFinite(position)) return "";
- const index = resolveTurbineIndexFromPosition(position);
- if (index < 0 || index > maxIndex) return "";
- if (Math.abs(position - index * TURBINE_Y_GAP) > 0.01) return "";
- if (!labeledIndex.has(index)) return "";
- return formatTurbineAxisLabel(index, indexToName);
- },
- };
- }
- function computePlotExtents(series = []) {
- let windMin = Infinity;
- let windMax = -Infinity;
- let powerMax = 0;
- series.forEach((item) => {
- (item.data || []).forEach((point) => {
- if (!Array.isArray(point) || point.length < 3) return;
- const [, windSpeed, power] = point;
- if (Number.isFinite(windSpeed)) {
- windMin = Math.min(windMin, windSpeed);
- windMax = Math.max(windMax, windSpeed);
- }
- if (Number.isFinite(power)) {
- powerMax = Math.max(powerMax, power);
- }
- });
- });
- if (!Number.isFinite(windMin) || !Number.isFinite(windMax)) {
- windMin = 0;
- windMax = 25;
- }
- if (powerMax <= 0) powerMax = 2000;
- const windPad = Math.max(0.5, (windMax - windMin) * 0.04);
- const powerPad = Math.max(50, powerMax * 0.06);
- return {
- windMin: Math.max(0, windMin - windPad),
- windMax: windMax + windPad,
- powerMax: powerMax + powerPad,
- };
- }
- function resolveWindSpeedAxisName(panel) {
- const name = String(panel?.xaxis || "").trim();
- if (/风速/.test(name)) return name;
- return WIND_POWER_3D_AXIS.y;
- }
- function resolvePowerAxisName(panel) {
- const name = String(panel?.yaxis || "").trim();
- if (/功率/.test(name)) return name;
- return WIND_POWER_3D_AXIS.z;
- }
- function buildGrid3DLayout({
- hasLegend,
- hasSubtitle,
- reportMode,
- }) {
- const titleBlock = hasSubtitle ? 50 : 40;
- return {
- boxWidth: reportMode ? 96 : 108,
- boxHeight: reportMode ? 88 : 96,
- boxDepth: reportMode ? 158 : 176,
- top: titleBlock,
- left: hasLegend ? 18 : "5%",
- right: hasLegend ? 112 : "5%",
- bottom: 72,
- viewControl: {
- projection: "orthographic",
- autoRotate: false,
- distance: reportMode ? 225 : 218,
- orthographicSize: reportMode ? 215 : 150,
- alpha: REFERENCE_VIEW_CONTROL.alpha,
- beta: REFERENCE_VIEW_CONTROL.beta,
- minAlpha: 8,
- maxAlpha: 62,
- rotateSensitivity: 1,
- zoomSensitivity: 1,
- panSensitivity: 0.8,
- center: [...REFERENCE_VIEW_CONTROL.center],
- },
- };
- }
- function defaultPickColor(label, index) {
- const palette = ["#1E7CF8", "#7C5CFF", "#16A34A", "#F59E0B", "#EF4444"];
- const text = String(label || "");
- if (/参考|合同|理论/.test(text)) return "#64748B";
- if (/异常/.test(text) && !/正常/.test(text)) return "#EF4444";
- if (/正常/.test(text)) return "#1E7CF8";
- return palette[index % palette.length];
- }
- /**
- * @param {object} params
- * @param {object} params.panel - { title, xaxis, yaxis, data }
- * @param {string} params.templateKey
- * @param {string} [params.title]
- * @param {boolean} [params.farmMode]
- * @param {function} [params.pickColor]
- * @param {object} [params.theme]
- * @param {boolean} [params.reportMode] - 报告白底样式
- */
- export function buildWindPower3DChartOption({
- panel,
- templateKey,
- title = "",
- farmMode = false,
- pickColor = defaultPickColor,
- theme = {},
- reportMode = false,
- }) {
- const rows = panel?.data || [];
- const turbineMeta = buildTurbineAxisMeta(rows);
- const turbineMax = Math.max(turbineMeta.maxIndex, 1);
- const turbineSeries = [];
- const referenceSeries = [];
- const legendNames = [];
- rows.forEach((row, index) => {
- const color = resolveFarmSeriesColor(
- row,
- index,
- turbineMeta,
- pickColor,
- farmMode,
- );
- const visual = resolveFarmSeriesVisual(row, templateKey, turbineMax, reportMode);
- if (isReferenceLineRow(row)) {
- const data = buildReferenceLineTriplets(row);
- if (!data.length) return;
- referenceSeries.push({
- name: row.label,
- type: "line3D",
- data,
- lineStyle: {
- width: visual.lineWidth,
- color,
- type: visual.lineType,
- opacity: visual.lineOpacity,
- },
- itemStyle: { color, opacity: visual.itemOpacity },
- });
- legendNames.push(row.label);
- return;
- }
- const triplets = resolveFarmTriplets(row, turbineMeta);
- if (!triplets.length) return;
- const useScatter = isMarkerRow(row, templateKey);
- const plotData = useScatter
- ? triplets
- : [...triplets].sort((a, b) => a[1] - b[1]);
- const item = {
- name: row.label,
- type: useScatter ? "scatter3D" : "line3D",
- data: plotData,
- shading: "color",
- symbolSize: visual.symbolSize,
- lineStyle: useScatter
- ? undefined
- : {
- width: visual.lineWidth,
- color,
- type: visual.lineType,
- opacity: visual.lineOpacity,
- },
- itemStyle: { color, opacity: visual.itemOpacity },
- };
- turbineSeries.push(item);
- legendNames.push(row.label);
- });
- const series = [...turbineSeries, ...referenceSeries];
- const plotExtents = computePlotExtents(series);
- const axisColor = theme.axis || (reportMode ? "#64748b" : "#8da3c7");
- const legendColor = theme.legend || (reportMode ? "#334155" : "#cde6ff");
- const titleColor = theme.title || (reportMode ? "#0f172a" : "#e3f2ff");
- const splitColor = theme.split || (reportMode ? "#e2e8f0" : "rgba(98, 121, 186, 0.16)");
- const bg = reportMode ? "#ffffff" : "transparent";
- const legendData = legendNames;
- const hasLegend = legendData.length > 1;
- const chartTitle = title || panel?.title || "";
- const chartSubtitle =
- farmMode && turbineMax > 8 && !reportMode ? `共 ${turbineMax} 台机组` : "";
- const hasReference = rows.some(isReferenceLineRow);
- const turbineAxisMax = (hasReference ? turbineMax + 0.5 : turbineMax) * TURBINE_Y_GAP;
- const turbineAxisLabel = buildTurbineAxisLabelConfig(
- turbineMax,
- turbineMeta.indexToName,
- hasReference,
- );
- const grid3DLayout = buildGrid3DLayout({
- hasLegend,
- hasSubtitle: Boolean(chartSubtitle),
- reportMode,
- });
- return {
- backgroundColor: bg,
- animation: reportMode ? false : true,
- animationDuration: reportMode ? 0 : undefined,
- title: chartTitle
- ? {
- text: chartTitle,
- subtext: chartSubtitle,
- left: "center",
- top: reportMode ? 8 : 4,
- textStyle: {
- fontSize: reportMode ? 13 : 12,
- color: titleColor,
- fontWeight: reportMode ? 600 : 500,
- },
- subtextStyle: {
- fontSize: 10,
- color: axisColor,
- opacity: 0.82,
- },
- itemGap: chartSubtitle ? 4 : 8,
- }
- : undefined,
- toolbox: reportMode
- ? undefined
- : {
- show: true,
- top: 4,
- right: hasLegend ? 112 : 12,
- itemSize: 14,
- itemGap: 10,
- iconStyle: {
- borderColor: axisColor,
- },
- emphasis: {
- iconStyle: {
- borderColor: titleColor,
- },
- },
- feature: {
- restore: {
- show: true,
- title: "还原视角",
- },
- saveAsImage: {
- show: true,
- title: "保存图片",
- name: chartTitle || "全场风功率图",
- backgroundColor: "#0f172a",
- pixelRatio: 2,
- },
- },
- },
- tooltip: {
- show: true,
- formatter(params) {
- if (!params) return "";
- const val = params.value || params.data;
- if (!Array.isArray(val)) return "";
- const [turbinePosition, windSpeed, power] = val;
- const turbineText = formatTurbineAxisLabel(
- resolveTurbineIndexFromPosition(turbinePosition),
- turbineMeta.indexToName,
- );
- return `${params.marker || ""}${params.seriesName || ""}<br/>${WIND_POWER_3D_AXIS.y}: ${windSpeed}<br/>${WIND_POWER_3D_AXIS.x}: ${turbineText}<br/>${WIND_POWER_3D_AXIS.z}: ${power}`;
- },
- },
- legend: hasLegend
- ? {
- type: "scroll",
- orient: "vertical",
- right: reportMode ? 4 : 0,
- top: chartTitle ? (reportMode ? 40 : 36) : 12,
- bottom: 8,
- width: 96,
- itemWidth: 12,
- itemHeight: 8,
- itemGap: 5,
- pageButtonItemGap: 6,
- pageIconSize: 10,
- textStyle: { color: legendColor, fontSize: 10 },
- data: legendData,
- }
- : undefined,
- grid3D: {
- ...grid3DLayout,
- light: {
- main: { intensity: 1.35, shadow: reportMode, alpha: 35, beta: 120 },
- ambient: { intensity: 0.92 },
- },
- },
- xAxis3D: {
- type: "value",
- name: WIND_POWER_3D_AXIS.x,
- min: 0,
- max: turbineAxisMax,
- interval: turbineAxisLabel.interval,
- nameGap: 28,
- nameTextStyle: { color: axisColor, fontSize: 10 },
- axisLabel: {
- color: axisColor,
- fontSize: 9,
- hideOverlap: true,
- showMinLabel: true,
- showMaxLabel: true,
- margin: 4,
- formatter: turbineAxisLabel.formatter,
- },
- axisLine: { lineStyle: { color: splitColor, opacity: 0.65 } },
- splitLine: buildAxisSplitStyle(splitColor, true),
- },
- yAxis3D: {
- type: "value",
- name: resolveWindSpeedAxisName(panel),
- min: plotExtents.windMin,
- max: plotExtents.windMax,
- splitNumber: 5,
- nameGap: 22,
- nameTextStyle: { color: axisColor, fontSize: 10, fontWeight: 500 },
- axisLabel: { color: axisColor, fontSize: 9, margin: 4 },
- axisLine: { lineStyle: { color: splitColor, opacity: 0.8 } },
- splitLine: buildAxisSplitStyle(splitColor, true),
- },
- zAxis3D: {
- type: "value",
- name: resolvePowerAxisName(panel),
- min: 0,
- max: plotExtents.powerMax,
- splitNumber: 5,
- nameGap: 24,
- nameTextStyle: { color: axisColor, fontSize: 10, fontWeight: 500 },
- axisLabel: { color: axisColor, fontSize: 9, margin: 4 },
- axisLine: { lineStyle: { color: splitColor, opacity: 0.8 } },
- splitLine: buildAxisSplitStyle(splitColor, true),
- },
- series,
- };
- }
- export function buildWindPower3DPanelOption(panel, options = {}) {
- return buildWindPower3DChartOption({
- panel,
- templateKey: options.templateKey || "",
- title: options.title || panel?.title || "",
- farmMode: Boolean(options.farmMode),
- pickColor: options.pickColor,
- theme: options.theme || {},
- reportMode: Boolean(options.reportMode),
- });
- }
|