/** * 风功率曲线 / 风功率散点 — 全场 3D 图表(前端 + 报告 Node 共用) * 展示坐标:X = 风机名称,Y = 风速,Z = 功率(竖直) * 原始 panel 数据仍为 xData=风速、yData=功率 */ import { classifyDiagnosticSeries, diagnosticSeriesColor, isSharedOverlayCurveSeries, isContractPowerCurve, DIAGNOSTIC_COLORS, DIAGNOSTIC_FARM_STYLES, } from "./anomalyDiagnosticVisual.mjs"; import { formatReportValueAxisLabel, isPointAnomalyFlag, readPointAnomalyFlags, resolvePanelSubtitle, } from "./anomalyDetectorPlotAdapter.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; /** 报告导出时更少标签,避免 3D 轴文字挤成一团 */ export const FARM_WIND_POWER_3D_REPORT_LABEL_COUNT = 6; /** 机组在 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 isPowerCurveAnomalyTurbine(row) { if (row?.anomaly === true || row?.extra?.anomaly === true) return true; if (row?.anomaly === false || row?.extra?.anomaly === false) return false; return isAnomalyRow(row); } /** 图例放在三维图右侧,避免压住底部机组轴和曲线 */ function buildSideLegend(names, { legendColor, top }) { if (!names.length) return null; return { type: names.length > 16 ? "scroll" : "plain", orient: "vertical", right: 6, top, bottom: 16, width: 118, itemWidth: 10, itemHeight: 10, icon: "circle", itemGap: 8, textStyle: { color: legendColor, fontSize: 11 }, formatter: (name) => String(name || "").replace(/\s+异常$/, ""), data: names, }; } function isAnomalyRow(row) { const text = `${row?.originalLabel || ""} ${row?.label || ""}`; return /异常/.test(text) && !/正常/.test(text); } /** 行上仍带 pointAnomaly 时按同下标拆成正常/异常,与报告总图一致 */ function splitScatterRowByPointAnomaly(row) { const xData = Array.isArray(row?.xData) ? row.xData : []; const yData = Array.isArray(row?.yData) ? row.yData : []; const len = Math.min(xData.length, yData.length); const alreadySplit = /正常|异常/.test(String(row?.originalLabel || "")) && !Array.isArray(row?.pointAnomaly); const flags = alreadySplit ? null : readPointAnomalyFlags(row); if (!len || !flags || !flags.length) return [row]; const normal = { x: [], y: [] }; const anomaly = { x: [], y: [] }; for (let i = 0; i < len; i += 1) { const bucket = isPointAnomalyFlag(flags[i]) ? anomaly : normal; bucket.x.push(xData[i]); bucket.y.push(yData[i]); } const baseLabel = String(row?.engineName || row?.label || "") .replace(/\s*(正常数据|异常点|正常|异常)$/u, "") .trim(); const make = (bucket, kind) => { if (!bucket.x.length) return null; return { ...row, label: `${baseLabel} ${kind}`.trim(), originalLabel: kind, xData: bucket.x, yData: bucket.y, pointAnomaly: undefined, anomaly: kind === "异常", extra: { ...(row.extra || {}), anomaly: kind === "异常" }, }; }; return [make(normal, "正常"), make(anomaly, "异常")].filter(Boolean); } function resolveFarmSeriesColor( row, index, turbineMeta, pickColor, farmMode, templateKey = "", ) { if (!farmMode) return pickColor(row.label, index, row, farmMode); const role = classifyDiagnosticSeries(row); if ( role === "contract" && (templateKey === "wind_power_curve" || templateKey === "wind_power_scatter") ) { return DIAGNOSTIC_COLORS.farmContract; } if ( role === "contract" || role === "reference" || role === "warning" || role === "mean" ) { return diagnosticSeriesColor(row, index); } if (typeof pickColor === "function") { return pickColor(row.label, index, row, farmMode); } // 无外部 pickColor 时:风功率等状态色检测器走正常蓝/异常红,其余保留渐变 // 全场不用单机降载黄/停机黑 if ( templateKey === "wind_power_curve" || templateKey === "wind_power_scatter" ) { return role === "anomaly" || role === "deload" || role === "shutdown" ? DIAGNOSTIC_COLORS.anomaly : DIAGNOSTIC_COLORS.normal; } if (role === "deload" || role === "shutdown") { return DIAGNOSTIC_COLORS.anomaly; } 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) { const contract = isContractPowerCurve(row); return { lineWidth: contract ? DIAGNOSTIC_FARM_STYLES.contract.lineWidth : 2.2, lineOpacity: 1, lineType: contract ? "solid" : "dashed", itemOpacity: 1, symbolSize: 3, }; } if (isScatter) { // 报告白底用小点、低透明度;页面深色底必须不透明,否则正常蓝点会消失 let symbolSize; let itemOpacity; if (reportMode && templateKey === "wind_power_scatter") { // 正常点略小、略淡;异常点只大一圈,纯红不描边 const normalSize = dense ? 4 : 5; symbolSize = isAnomaly ? normalSize + 2 : normalSize; itemOpacity = isAnomaly ? 1 : dense ? 0.45 : 0.62; } else if (reportMode) { symbolSize = dense ? 2 : 3; itemOpacity = dense ? isAnomaly ? 0.68 : 0.4 : isAnomaly ? 0.85 : 0.55; } else { symbolSize = dense ? 3 : 4; itemOpacity = 1; } return { lineWidth: 1, lineOpacity: 1, lineType: "solid", itemOpacity, symbolSize, }; } 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: dense ? (reportMode ? 0 : 1.5) : 2, }; } 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; return isSharedOverlayCurveSeries(row); } 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}号`; } /** 机组较多时风机名称轴仅抽样展示,避免标签重叠 */ function buildTurbineAxisLabelConfig( maxIndex, indexToName, reportMode = false, ) { const maxLabels = reportMode ? FARM_WIND_POWER_3D_REPORT_LABEL_COUNT : FARM_WIND_POWER_3D_TURBINE_LABEL_COUNT; const labeledIndex = new Set(); if (maxIndex <= maxLabels) { for (let index = 1; index <= maxIndex; index += 1) labeledIndex.add(index); } else { labeledIndex.add(1); labeledIndex.add(maxIndex); const reserved = labeledIndex.size; const slots = Math.max(0, maxLabels - reserved); if (slots > 0 && maxIndex > 2) { 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 >= maxLabels) break; } } } const labelMap = {}; labeledIndex.forEach((index) => { labelMap[String(index * TURBINE_Y_GAP)] = formatTurbineAxisLabel( index, indexToName, ); }); return { interval: TURBINE_Y_GAP, labeledIndex, labelMap, gap: TURBINE_Y_GAP, 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 powerMin = Infinity; let powerMax = -Infinity; 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)) { powerMin = Math.min(powerMin, power); powerMax = Math.max(powerMax, power); } }); }); if (!Number.isFinite(windMin) || !Number.isFinite(windMax)) { windMin = 0; windMax = 25; } if (!Number.isFinite(powerMin) || !Number.isFinite(powerMax)) { powerMin = 0; powerMax = 2000; } const windPad = Math.max(0.5, (windMax - windMin) * 0.04); const powerPad = Math.max(50, Math.abs(powerMax - powerMin) * 0.06); return { windMin: windMin - windPad, windMax: windMax + windPad, powerMin: powerMin - powerPad, 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, legendRows = 0, hasSubtitle, reportMode, templateKey = "", legendSide = false, }) { const titleBlock = hasSubtitle ? 50 : 40; const reportLook = reportMode || templateKey === "wind_power_scatter"; const bottom = legendSide ? 52 : legendRows > 1 ? 108 : legendRows === 1 ? 86 : 72; return { boxWidth: reportLook ? 96 : 108, boxHeight: reportLook ? 88 : 96, boxDepth: reportLook ? 158 : 176, top: titleBlock, left: legendSide ? 4 : hasLegend ? 18 : "5%", right: legendSide ? 156 : hasLegend ? 112 : "5%", bottom, viewControl: { projection: "orthographic", autoRotate: false, distance: reportLook ? 225 : 218, orthographicSize: reportLook ? 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 DIAGNOSTIC_COLORS.contract; 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 sourceRows = panel?.data || []; const isFarmPowerCurve = farmMode && templateKey === "wind_power_curve"; // JSON 里的机组曲线全部绘制。报告图例不列出正常机组。 const rows = sourceRows; const turbineMeta = buildTurbineAxisMeta(rows); const turbineMax = Math.max(turbineMeta.maxIndex, 1); const turbineSeries = []; const referenceSeries = []; const legendNames = []; rows.forEach((sourceRow, index) => { const expanded = templateKey === "wind_power_scatter" ? splitScatterRowByPointAnomaly(sourceRow) : [sourceRow]; expanded.forEach((row) => { let color = resolveFarmSeriesColor( row, index, turbineMeta, pickColor, farmMode, templateKey, ); if ( reportMode && templateKey === "wind_power_scatter" && isAnomalyRow(row) && !isReferenceLineRow(row) ) { color = "#FF1E1E"; } 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 seriesName = isFarmPowerCurve && isPowerCurveAnomalyTurbine(row) ? String(row.engineName || resolveTurbineName(row) || row.label || "") : row.label; const useScatter = isMarkerRow(row, templateKey); const plotData = useScatter ? triplets : [...triplets].sort((a, b) => a[1] - b[1]); const item = { name: seriesName, type: useScatter ? "scatter3D" : "line3D", data: plotData, shading: "color", symbol: "circle", symbolSize: visual.symbolSize, lineStyle: useScatter ? undefined : { width: visual.lineWidth, color, type: visual.lineType, opacity: visual.lineOpacity, }, itemStyle: { color, opacity: visual.itemOpacity, ...(visual.borderWidth ? { borderWidth: visual.borderWidth, borderColor: visual.borderColor || "#ffffff", } : {}), }, }; turbineSeries.push(item); const hideNormalLegend = reportMode && isFarmPowerCurve && !isPowerCurveAnomalyTurbine(row); if (!hideNormalLegend) legendNames.push(seriesName); }); }); turbineSeries.sort((a, b) => { const rank = (name) => /异常/.test(String(name || "")) && !/正常/.test(String(name || "")) ? 1 : 0; return rank(a.name) - rank(b.name); }); 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 ? "#eef4fb" : "transparent"; const legendData = legendNames; const chartTitle = title || panel?.title || ""; const sideLegend = isFarmPowerCurve ? buildSideLegend(legendData, { legendColor, top: chartTitle ? (reportMode ? 36 : 40) : 12, }) : null; const hasLegend = sideLegend ? legendData.length > 0 : legendData.length > 1; const chartSubtitle = resolvePanelSubtitle(panel); const hasReference = rows.some(isReferenceLineRow); const turbineAxisMax = (hasReference ? turbineMax + 0.5 : turbineMax) * TURBINE_Y_GAP; const turbineAxisLabel = buildTurbineAxisLabelConfig( turbineMax, turbineMeta.indexToName, reportMode, ); const grid3DLayout = buildGrid3DLayout({ hasLegend, legendSide: Boolean(sideLegend), hasSubtitle: Boolean(chartSubtitle), reportMode, templateKey, }); 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: sideLegend ? 168 : 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, ); const turbineLine = turbineText ? `
${WIND_POWER_3D_AXIS.x}: ${turbineText}` : ""; return `${params.marker || ""}${params.seriesName || ""}
${WIND_POWER_3D_AXIS.y}: ${windSpeed}${turbineLine}
${WIND_POWER_3D_AXIS.z}: ${power}`; }, }, legend: sideLegend ? sideLegend : hasLegend ? { type: "scroll", orient: "vertical", right: reportMode ? 4 : 0, top: chartTitle ? (reportMode ? 40 : 36) : 12, bottom: 8, width: 96, itemWidth: 12, itemHeight: 12, icon: "circle", itemGap: 5, pageButtonItemGap: 6, pageIconSize: 10, textStyle: { color: legendColor, fontSize: 10 }, data: legendData, } : undefined, grid3D: { ...grid3DLayout, light: { main: { intensity: 1.35, shadow: reportMode && templateKey !== "wind_power_scatter", alpha: 35, beta: 120, }, ambient: { intensity: reportMode && templateKey === "wind_power_scatter" ? 1.05 : 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 }, // Puppeteer 无法序列化 formatter,报告侧靠此字段重建抽样标签 _turbineLabelMeta: { gap: turbineAxisLabel.gap, labels: turbineAxisLabel.labelMap, }, axisLabel: { color: axisColor, fontSize: reportMode ? 8 : 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, formatter: reportMode ? (value) => formatReportValueAxisLabel(value) : undefined, }, axisLine: { lineStyle: { color: splitColor, opacity: 0.8 } }, splitLine: buildAxisSplitStyle(splitColor, true), }, zAxis3D: { type: "value", name: resolvePowerAxisName(panel), min: plotExtents.powerMin, max: plotExtents.powerMax, splitNumber: 5, nameGap: 24, nameTextStyle: { color: axisColor, fontSize: 10, fontWeight: 500 }, axisLabel: { color: axisColor, fontSize: 9, margin: 4, formatter: reportMode ? (value) => formatReportValueAxisLabel(value) : undefined, }, 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), }); }