anomalyWindPower3D.mjs 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610
  1. /**
  2. * 风功率曲线 / 风功率散点 — 全场 3D 图表(前端 + 报告 Node 共用)
  3. * 展示坐标:X = 风机机位,Y = 风速,Z = 功率(竖直)
  4. * 原始 panel 数据仍为 xData=风速、yData=功率
  5. */
  6. import {
  7. classifyDiagnosticSeries,
  8. diagnosticSeriesColor,
  9. } from "./anomalyDiagnosticVisual.mjs";
  10. export const WIND_POWER_3D_TEMPLATE_KEYS = new Set([
  11. "wind_power_curve",
  12. "wind_power_scatter",
  13. ]);
  14. export const WIND_POWER_3D_AXIS = {
  15. x: "风机机位",
  16. y: "风速 (m/s)",
  17. z: "有功功率 (kW)",
  18. };
  19. /** 参考图视角:功率轴竖直,风机与风速轴分别向左、右前方展开 */
  20. const REFERENCE_VIEW_CONTROL = {
  21. alpha: 25,
  22. beta: 45,
  23. center: [0, -4, 0],
  24. };
  25. /** Y 轴(机组)最多展示的名称数(折线/散点数据仍全部绘制) */
  26. export const FARM_WIND_POWER_3D_TURBINE_LABEL_COUNT = 8;
  27. /** 机组在 Y 轴上的间距(数值越大,各风机切片越分开) */
  28. const TURBINE_Y_GAP = 10;
  29. /** 全场 3D 渐变色:提高饱和度与明度,避免深色底上看不清 */
  30. function farmTurbineGradientColor(turbineIndex, maxIndex, isAnomaly = false) {
  31. const t =
  32. maxIndex <= 1 ? 0 : Math.max(0, Math.min(1, (turbineIndex - 1) / (maxIndex - 1)));
  33. const hue = 88 + t * 172;
  34. if (isAnomaly) return `hsla(${hue}, 92%, 64%, 1)`;
  35. return `hsla(${hue}, 88%, 66%, 1)`;
  36. }
  37. function isAnomalyRow(row) {
  38. const text = `${row?.originalLabel || ""} ${row?.label || ""}`;
  39. return /异常/.test(text) && !/正常/.test(text);
  40. }
  41. function resolveFarmSeriesColor(row, index, turbineMeta, pickColor, farmMode) {
  42. if (!farmMode) return pickColor(row.label, index, row, farmMode);
  43. const role = classifyDiagnosticSeries(row);
  44. if (role === "reference" || role === "warning" || role === "mean") {
  45. return diagnosticSeriesColor(row, index);
  46. }
  47. if (typeof pickColor === "function") {
  48. return pickColor(row.label, index, row, farmMode);
  49. }
  50. const ti = resolveTurbineIndex(row, turbineMeta);
  51. return farmTurbineGradientColor(ti, turbineMeta.maxIndex, role === "anomaly");
  52. }
  53. function resolveFarmSeriesVisual(row, templateKey, turbineMax, reportMode) {
  54. const isRef = isReferenceLineRow(row);
  55. const isScatter = !isRef && isMarkerRow(row, templateKey);
  56. const isAnomaly = isAnomalyRow(row);
  57. const dense = turbineMax > 10;
  58. if (isRef) {
  59. return {
  60. lineWidth: 2.2,
  61. lineOpacity: 1,
  62. lineType: "dashed",
  63. itemOpacity: 1,
  64. symbolSize: 3,
  65. };
  66. }
  67. if (isScatter) {
  68. return {
  69. lineWidth: 1,
  70. lineOpacity: 1,
  71. lineType: "solid",
  72. itemOpacity: dense ? (isAnomaly ? 0.95 : 0.82) : 0.95,
  73. symbolSize: dense ? (isAnomaly ? 6.5 : 5.5) : 7,
  74. };
  75. }
  76. return {
  77. lineWidth: dense ? (reportMode ? 2 : 1.9) : 2.4,
  78. lineOpacity: isAnomaly ? 1 : dense ? 0.92 : 0.98,
  79. lineType: "solid",
  80. itemOpacity: isAnomaly ? 1 : 0.92,
  81. symbolSize: 3,
  82. };
  83. }
  84. function buildAxisSplitStyle(splitColor, show = true) {
  85. return {
  86. show,
  87. lineStyle: { color: splitColor, type: "solid", opacity: show ? 0.35 : 0 },
  88. };
  89. }
  90. export function isWindPower3DTemplate(templateKey) {
  91. return WIND_POWER_3D_TEMPLATE_KEYS.has(String(templateKey || ""));
  92. }
  93. /** 仅全场(farmMode)风功率曲线/散点使用 3D,单机保持 2D */
  94. export function shouldUseWindPower3D(templateKey, farmMode = false) {
  95. return Boolean(farmMode) && isWindPower3DTemplate(templateKey);
  96. }
  97. function getRowArrays(row) {
  98. const xData = Array.isArray(row?.xData) ? row.xData : [];
  99. const yData = Array.isArray(row?.yData) ? row.yData : [];
  100. const zData = Array.isArray(row?.zData) ? row.zData : [];
  101. return { xData, yData, zData };
  102. }
  103. function formatEngineLabel(name) {
  104. if (name == null || name === "") return "";
  105. const text = String(name);
  106. if (/号$/.test(text)) return text;
  107. if (/^\d+$/.test(text)) return `${text}号`;
  108. return text;
  109. }
  110. function resolveTurbineName(row) {
  111. if (row?.engineName) return formatEngineLabel(row.engineName);
  112. const label = String(row?.label || "");
  113. const head = label.split(/\s+/)[0] || "";
  114. if (/号/.test(head) || /^\d+$/.test(head)) return formatEngineLabel(head);
  115. return head;
  116. }
  117. function buildTurbineAxisMeta(rows = []) {
  118. const nameToIndex = new Map();
  119. let maxIndex = 0;
  120. rows.forEach((row) => {
  121. if (isReferenceLineRow(row)) return;
  122. const name = resolveTurbineName(row);
  123. let turbineIndex =
  124. row?.turbineIndex != null && Number.isFinite(Number(row.turbineIndex))
  125. ? Number(row.turbineIndex) + 1
  126. : null;
  127. if (turbineIndex == null && name) {
  128. if (!nameToIndex.has(name)) {
  129. maxIndex += 1;
  130. nameToIndex.set(name, maxIndex);
  131. }
  132. turbineIndex = nameToIndex.get(name);
  133. }
  134. if (turbineIndex != null) {
  135. maxIndex = Math.max(maxIndex, turbineIndex);
  136. if (name) nameToIndex.set(name, turbineIndex);
  137. }
  138. });
  139. const indexToName = new Map();
  140. nameToIndex.forEach((index, name) => indexToName.set(index, name));
  141. return { maxIndex, indexToName, nameToIndex };
  142. }
  143. function resolveTurbineIndex(row, meta) {
  144. if (row?.turbineIndex != null && Number.isFinite(Number(row.turbineIndex))) {
  145. return Number(row.turbineIndex) + 1;
  146. }
  147. const name = resolveTurbineName(row);
  148. if (name && meta.nameToIndex.has(name)) return meta.nameToIndex.get(name);
  149. return 1;
  150. }
  151. /** ECharts 3D 坐标:[机组位置, 风速, 功率] */
  152. function toChartPoint(windSpeed, power, turbineIndex) {
  153. return [turbineIndex * TURBINE_Y_GAP, windSpeed, power];
  154. }
  155. function resolveTurbineIndexFromPosition(position) {
  156. return Math.round(Number(position) / TURBINE_Y_GAP);
  157. }
  158. function resolveFarmTriplets(row, meta) {
  159. const { xData, yData, zData } = getRowArrays(row);
  160. const powerArr = zData.length ? zData : yData;
  161. const turbineIndex = resolveTurbineIndex(row, meta);
  162. const len = Math.min(xData.length, powerArr.length);
  163. const triplets = [];
  164. for (let i = 0; i < len; i += 1) {
  165. const windSpeed = Number(xData[i]);
  166. const power = Number(powerArr[i]);
  167. if (!Number.isFinite(windSpeed) || !Number.isFinite(power)) continue;
  168. triplets.push(toChartPoint(windSpeed, power, turbineIndex));
  169. }
  170. return triplets;
  171. }
  172. function isReferenceLineRow(row) {
  173. if (row?.__shared) return true;
  174. const text = `${row?.originalLabel || ""} ${row?.label || ""}`;
  175. return /参考功率|理论功率|合同功率|参考曲线/.test(text);
  176. }
  177. function isMarkerRow(row, templateKey) {
  178. if (isReferenceLineRow(row)) return false;
  179. if (templateKey === "wind_power_curve") return false;
  180. if (templateKey === "wind_power_scatter") return true;
  181. const mode = String(row?.mode || "lines");
  182. return mode.includes("markers") && !mode.includes("lines");
  183. }
  184. function buildReferenceLineTriplets(row) {
  185. const { xData, yData, zData } = getRowArrays(row);
  186. const powerArr = zData.length ? zData : yData;
  187. const len = Math.min(xData.length, powerArr.length);
  188. const triplets = [];
  189. for (let i = 0; i < len; i += 1) {
  190. const windSpeed = Number(xData[i]);
  191. const power = Number(powerArr[i]);
  192. if (!Number.isFinite(windSpeed) || !Number.isFinite(power)) continue;
  193. triplets.push(toChartPoint(windSpeed, power, 0));
  194. }
  195. return triplets;
  196. }
  197. function formatTurbineAxisLabel(index, indexToName) {
  198. if (index === 0) return "参考";
  199. return indexToName.get(index) || `${index}号`;
  200. }
  201. /** 机组较多时 Y 轴仅抽样展示名称,避免标签重叠 */
  202. function buildTurbineAxisLabelConfig(maxIndex, indexToName, hasReference) {
  203. const labeledIndex = new Set();
  204. if (hasReference) labeledIndex.add(0);
  205. if (maxIndex <= FARM_WIND_POWER_3D_TURBINE_LABEL_COUNT) {
  206. for (let index = 1; index <= maxIndex; index += 1) labeledIndex.add(index);
  207. } else {
  208. labeledIndex.add(1);
  209. labeledIndex.add(maxIndex);
  210. const slots = FARM_WIND_POWER_3D_TURBINE_LABEL_COUNT - labeledIndex.size;
  211. const step = Math.max(1, Math.round((maxIndex - 1) / (slots + 1)));
  212. for (let index = 1 + step; index < maxIndex; index += step) {
  213. labeledIndex.add(index);
  214. if (labeledIndex.size >= FARM_WIND_POWER_3D_TURBINE_LABEL_COUNT) break;
  215. }
  216. }
  217. const labelInterval = TURBINE_Y_GAP;
  218. return {
  219. interval: labelInterval,
  220. labeledIndex,
  221. formatter(v) {
  222. const position = Number(v);
  223. if (!Number.isFinite(position)) return "";
  224. const index = resolveTurbineIndexFromPosition(position);
  225. if (index < 0 || index > maxIndex) return "";
  226. if (Math.abs(position - index * TURBINE_Y_GAP) > 0.01) return "";
  227. if (!labeledIndex.has(index)) return "";
  228. return formatTurbineAxisLabel(index, indexToName);
  229. },
  230. };
  231. }
  232. function computePlotExtents(series = []) {
  233. let windMin = Infinity;
  234. let windMax = -Infinity;
  235. let powerMax = 0;
  236. series.forEach((item) => {
  237. (item.data || []).forEach((point) => {
  238. if (!Array.isArray(point) || point.length < 3) return;
  239. const [, windSpeed, power] = point;
  240. if (Number.isFinite(windSpeed)) {
  241. windMin = Math.min(windMin, windSpeed);
  242. windMax = Math.max(windMax, windSpeed);
  243. }
  244. if (Number.isFinite(power)) {
  245. powerMax = Math.max(powerMax, power);
  246. }
  247. });
  248. });
  249. if (!Number.isFinite(windMin) || !Number.isFinite(windMax)) {
  250. windMin = 0;
  251. windMax = 25;
  252. }
  253. if (powerMax <= 0) powerMax = 2000;
  254. const windPad = Math.max(0.5, (windMax - windMin) * 0.04);
  255. const powerPad = Math.max(50, powerMax * 0.06);
  256. return {
  257. windMin: Math.max(0, windMin - windPad),
  258. windMax: windMax + windPad,
  259. powerMax: powerMax + powerPad,
  260. };
  261. }
  262. function resolveWindSpeedAxisName(panel) {
  263. const name = String(panel?.xaxis || "").trim();
  264. if (/风速/.test(name)) return name;
  265. return WIND_POWER_3D_AXIS.y;
  266. }
  267. function resolvePowerAxisName(panel) {
  268. const name = String(panel?.yaxis || "").trim();
  269. if (/功率/.test(name)) return name;
  270. return WIND_POWER_3D_AXIS.z;
  271. }
  272. function buildGrid3DLayout({
  273. hasLegend,
  274. hasSubtitle,
  275. reportMode,
  276. }) {
  277. const titleBlock = hasSubtitle ? 50 : 40;
  278. return {
  279. boxWidth: reportMode ? 96 : 108,
  280. boxHeight: reportMode ? 88 : 96,
  281. boxDepth: reportMode ? 158 : 176,
  282. top: titleBlock,
  283. left: hasLegend ? 18 : "5%",
  284. right: hasLegend ? 112 : "5%",
  285. bottom: 72,
  286. viewControl: {
  287. projection: "orthographic",
  288. autoRotate: false,
  289. distance: reportMode ? 225 : 218,
  290. orthographicSize: reportMode ? 215 : 150,
  291. alpha: REFERENCE_VIEW_CONTROL.alpha,
  292. beta: REFERENCE_VIEW_CONTROL.beta,
  293. minAlpha: 8,
  294. maxAlpha: 62,
  295. rotateSensitivity: 1,
  296. zoomSensitivity: 1,
  297. panSensitivity: 0.8,
  298. center: [...REFERENCE_VIEW_CONTROL.center],
  299. },
  300. };
  301. }
  302. function defaultPickColor(label, index) {
  303. const palette = ["#1E7CF8", "#7C5CFF", "#16A34A", "#F59E0B", "#EF4444"];
  304. const text = String(label || "");
  305. if (/参考|合同|理论/.test(text)) return "#64748B";
  306. if (/异常/.test(text) && !/正常/.test(text)) return "#EF4444";
  307. if (/正常/.test(text)) return "#1E7CF8";
  308. return palette[index % palette.length];
  309. }
  310. /**
  311. * @param {object} params
  312. * @param {object} params.panel - { title, xaxis, yaxis, data }
  313. * @param {string} params.templateKey
  314. * @param {string} [params.title]
  315. * @param {boolean} [params.farmMode]
  316. * @param {function} [params.pickColor]
  317. * @param {object} [params.theme]
  318. * @param {boolean} [params.reportMode] - 报告白底样式
  319. */
  320. export function buildWindPower3DChartOption({
  321. panel,
  322. templateKey,
  323. title = "",
  324. farmMode = false,
  325. pickColor = defaultPickColor,
  326. theme = {},
  327. reportMode = false,
  328. }) {
  329. const rows = panel?.data || [];
  330. const turbineMeta = buildTurbineAxisMeta(rows);
  331. const turbineMax = Math.max(turbineMeta.maxIndex, 1);
  332. const turbineSeries = [];
  333. const referenceSeries = [];
  334. const legendNames = [];
  335. rows.forEach((row, index) => {
  336. const color = resolveFarmSeriesColor(
  337. row,
  338. index,
  339. turbineMeta,
  340. pickColor,
  341. farmMode,
  342. );
  343. const visual = resolveFarmSeriesVisual(row, templateKey, turbineMax, reportMode);
  344. if (isReferenceLineRow(row)) {
  345. const data = buildReferenceLineTriplets(row);
  346. if (!data.length) return;
  347. referenceSeries.push({
  348. name: row.label,
  349. type: "line3D",
  350. data,
  351. lineStyle: {
  352. width: visual.lineWidth,
  353. color,
  354. type: visual.lineType,
  355. opacity: visual.lineOpacity,
  356. },
  357. itemStyle: { color, opacity: visual.itemOpacity },
  358. });
  359. legendNames.push(row.label);
  360. return;
  361. }
  362. const triplets = resolveFarmTriplets(row, turbineMeta);
  363. if (!triplets.length) return;
  364. const useScatter = isMarkerRow(row, templateKey);
  365. const plotData = useScatter
  366. ? triplets
  367. : [...triplets].sort((a, b) => a[1] - b[1]);
  368. const item = {
  369. name: row.label,
  370. type: useScatter ? "scatter3D" : "line3D",
  371. data: plotData,
  372. shading: "color",
  373. symbolSize: visual.symbolSize,
  374. lineStyle: useScatter
  375. ? undefined
  376. : {
  377. width: visual.lineWidth,
  378. color,
  379. type: visual.lineType,
  380. opacity: visual.lineOpacity,
  381. },
  382. itemStyle: { color, opacity: visual.itemOpacity },
  383. };
  384. turbineSeries.push(item);
  385. legendNames.push(row.label);
  386. });
  387. const series = [...turbineSeries, ...referenceSeries];
  388. const plotExtents = computePlotExtents(series);
  389. const axisColor = theme.axis || (reportMode ? "#64748b" : "#8da3c7");
  390. const legendColor = theme.legend || (reportMode ? "#334155" : "#cde6ff");
  391. const titleColor = theme.title || (reportMode ? "#0f172a" : "#e3f2ff");
  392. const splitColor = theme.split || (reportMode ? "#e2e8f0" : "rgba(98, 121, 186, 0.16)");
  393. const bg = reportMode ? "#ffffff" : "transparent";
  394. const legendData = legendNames;
  395. const hasLegend = legendData.length > 1;
  396. const chartTitle = title || panel?.title || "";
  397. const chartSubtitle =
  398. farmMode && turbineMax > 8 && !reportMode ? `共 ${turbineMax} 台机组` : "";
  399. const hasReference = rows.some(isReferenceLineRow);
  400. const turbineAxisMax = (hasReference ? turbineMax + 0.5 : turbineMax) * TURBINE_Y_GAP;
  401. const turbineAxisLabel = buildTurbineAxisLabelConfig(
  402. turbineMax,
  403. turbineMeta.indexToName,
  404. hasReference,
  405. );
  406. const grid3DLayout = buildGrid3DLayout({
  407. hasLegend,
  408. hasSubtitle: Boolean(chartSubtitle),
  409. reportMode,
  410. });
  411. return {
  412. backgroundColor: bg,
  413. animation: reportMode ? false : true,
  414. animationDuration: reportMode ? 0 : undefined,
  415. title: chartTitle
  416. ? {
  417. text: chartTitle,
  418. subtext: chartSubtitle,
  419. left: "center",
  420. top: reportMode ? 8 : 4,
  421. textStyle: {
  422. fontSize: reportMode ? 13 : 12,
  423. color: titleColor,
  424. fontWeight: reportMode ? 600 : 500,
  425. },
  426. subtextStyle: {
  427. fontSize: 10,
  428. color: axisColor,
  429. opacity: 0.82,
  430. },
  431. itemGap: chartSubtitle ? 4 : 8,
  432. }
  433. : undefined,
  434. toolbox: reportMode
  435. ? undefined
  436. : {
  437. show: true,
  438. top: 4,
  439. right: hasLegend ? 112 : 12,
  440. itemSize: 14,
  441. itemGap: 10,
  442. iconStyle: {
  443. borderColor: axisColor,
  444. },
  445. emphasis: {
  446. iconStyle: {
  447. borderColor: titleColor,
  448. },
  449. },
  450. feature: {
  451. restore: {
  452. show: true,
  453. title: "还原视角",
  454. },
  455. saveAsImage: {
  456. show: true,
  457. title: "保存图片",
  458. name: chartTitle || "全场风功率图",
  459. backgroundColor: "#0f172a",
  460. pixelRatio: 2,
  461. },
  462. },
  463. },
  464. tooltip: {
  465. show: true,
  466. formatter(params) {
  467. if (!params) return "";
  468. const val = params.value || params.data;
  469. if (!Array.isArray(val)) return "";
  470. const [turbinePosition, windSpeed, power] = val;
  471. const turbineText = formatTurbineAxisLabel(
  472. resolveTurbineIndexFromPosition(turbinePosition),
  473. turbineMeta.indexToName,
  474. );
  475. 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}`;
  476. },
  477. },
  478. legend: hasLegend
  479. ? {
  480. type: "scroll",
  481. orient: "vertical",
  482. right: reportMode ? 4 : 0,
  483. top: chartTitle ? (reportMode ? 40 : 36) : 12,
  484. bottom: 8,
  485. width: 96,
  486. itemWidth: 12,
  487. itemHeight: 8,
  488. itemGap: 5,
  489. pageButtonItemGap: 6,
  490. pageIconSize: 10,
  491. textStyle: { color: legendColor, fontSize: 10 },
  492. data: legendData,
  493. }
  494. : undefined,
  495. grid3D: {
  496. ...grid3DLayout,
  497. light: {
  498. main: { intensity: 1.35, shadow: reportMode, alpha: 35, beta: 120 },
  499. ambient: { intensity: 0.92 },
  500. },
  501. },
  502. xAxis3D: {
  503. type: "value",
  504. name: WIND_POWER_3D_AXIS.x,
  505. min: 0,
  506. max: turbineAxisMax,
  507. interval: turbineAxisLabel.interval,
  508. nameGap: 28,
  509. nameTextStyle: { color: axisColor, fontSize: 10 },
  510. axisLabel: {
  511. color: axisColor,
  512. fontSize: 9,
  513. hideOverlap: true,
  514. showMinLabel: true,
  515. showMaxLabel: true,
  516. margin: 4,
  517. formatter: turbineAxisLabel.formatter,
  518. },
  519. axisLine: { lineStyle: { color: splitColor, opacity: 0.65 } },
  520. splitLine: buildAxisSplitStyle(splitColor, true),
  521. },
  522. yAxis3D: {
  523. type: "value",
  524. name: resolveWindSpeedAxisName(panel),
  525. min: plotExtents.windMin,
  526. max: plotExtents.windMax,
  527. splitNumber: 5,
  528. nameGap: 22,
  529. nameTextStyle: { color: axisColor, fontSize: 10, fontWeight: 500 },
  530. axisLabel: { color: axisColor, fontSize: 9, margin: 4 },
  531. axisLine: { lineStyle: { color: splitColor, opacity: 0.8 } },
  532. splitLine: buildAxisSplitStyle(splitColor, true),
  533. },
  534. zAxis3D: {
  535. type: "value",
  536. name: resolvePowerAxisName(panel),
  537. min: 0,
  538. max: plotExtents.powerMax,
  539. splitNumber: 5,
  540. nameGap: 24,
  541. nameTextStyle: { color: axisColor, fontSize: 10, fontWeight: 500 },
  542. axisLabel: { color: axisColor, fontSize: 9, margin: 4 },
  543. axisLine: { lineStyle: { color: splitColor, opacity: 0.8 } },
  544. splitLine: buildAxisSplitStyle(splitColor, true),
  545. },
  546. series,
  547. };
  548. }
  549. export function buildWindPower3DPanelOption(panel, options = {}) {
  550. return buildWindPower3DChartOption({
  551. panel,
  552. templateKey: options.templateKey || "",
  553. title: options.title || panel?.title || "",
  554. farmMode: Boolean(options.farmMode),
  555. pickColor: options.pickColor,
  556. theme: options.theme || {},
  557. reportMode: Boolean(options.reportMode),
  558. });
  559. }