功能: 支持功率曲线手动恢复滤除点
- 增加额定拐角过渡点保护,减少过度筛除 - 补齐滤除点导出字段 - 增加功率曲线编辑模式和笔刷恢复交互
This commit is contained in:
@@ -101,6 +101,11 @@ struct SchemeInfo {
|
||||
double rated_power = 2000.0;
|
||||
};
|
||||
|
||||
constexpr double kRatedCornerWindBefore = 0.5;
|
||||
constexpr double kRatedCornerWindAfter = 1.2;
|
||||
constexpr double kRatedCornerPowerLowerRatio = 0.88;
|
||||
constexpr double kRatedCornerPowerUpperRatio = 1.03;
|
||||
|
||||
std::string Trim(const std::string& value) {
|
||||
const auto begin = value.find_first_not_of(" \t\r\n");
|
||||
if (begin == std::string::npos) {
|
||||
@@ -485,8 +490,20 @@ void ComputeWindSpeed600sAverage(std::vector<ValidRow>& rows) {
|
||||
}
|
||||
}
|
||||
|
||||
bool IsRatedTransitionCorner(const ValidRow& row, const EstimatedParams& params) {
|
||||
if (params.rated_power <= 0.0 || params.rated_wind_speed <= 0.0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return row.wind_speed >= params.rated_wind_speed - kRatedCornerWindBefore &&
|
||||
row.wind_speed <= params.rated_wind_speed + kRatedCornerWindAfter &&
|
||||
row.active_power >= params.rated_power * kRatedCornerPowerLowerRatio &&
|
||||
row.active_power <= params.rated_power * kRatedCornerPowerUpperRatio;
|
||||
}
|
||||
|
||||
std::vector<ValidRow> FilterLimitPower(const std::vector<ValidRow>& rows,
|
||||
const CalculationOptions& options,
|
||||
const EstimatedParams* transition_params,
|
||||
int& removed_count,
|
||||
std::vector<RemovedPoint>& removed_points) {
|
||||
removed_count = 0;
|
||||
@@ -529,13 +546,15 @@ std::vector<ValidRow> FilterLimitPower(const std::vector<ValidRow>& rows,
|
||||
std::vector<ValidRow> result;
|
||||
result.reserve(rows.size());
|
||||
for (size_t i = 0; i < rows.size(); ++i) {
|
||||
if (remove_indexes.count(i) == 0) {
|
||||
const bool protect_transition =
|
||||
transition_params != nullptr && IsRatedTransitionCorner(rows[i], *transition_params);
|
||||
if (remove_indexes.count(i) == 0 || protect_transition) {
|
||||
result.push_back(rows[i]);
|
||||
} else {
|
||||
removed_points.push_back(RemovedPoint{rows[i], "limit_power"});
|
||||
++removed_count;
|
||||
}
|
||||
}
|
||||
removed_count = static_cast<int>(remove_indexes.size());
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -607,7 +626,8 @@ std::vector<ValidRow> FilterByWindBinIqr(const std::vector<ValidRow>& rows,
|
||||
ValueGetter value_getter,
|
||||
int& removed_count,
|
||||
std::vector<RemovedPoint>& removed_points,
|
||||
const std::string& reason) {
|
||||
const std::string& reason,
|
||||
const EstimatedParams* transition_params = nullptr) {
|
||||
removed_count = 0;
|
||||
if (rows.empty()) {
|
||||
return {};
|
||||
@@ -649,6 +669,9 @@ std::vector<ValidRow> FilterByWindBinIqr(const std::vector<ValidRow>& rows,
|
||||
const double value = value_getter(row);
|
||||
if (value >= lower && value <= upper) {
|
||||
result.push_back(row);
|
||||
} else if (transition_params != nullptr &&
|
||||
IsRatedTransitionCorner(row, *transition_params)) {
|
||||
result.push_back(row);
|
||||
} else {
|
||||
++removed_count;
|
||||
removed_points.push_back(RemovedPoint{row, reason});
|
||||
@@ -768,9 +791,10 @@ std::vector<ValidRow> FilterHighWindLowPower(const std::vector<ValidRow>& rows,
|
||||
return rows;
|
||||
}
|
||||
|
||||
const double high_wind_start = params.rated_wind_speed + 1.0;
|
||||
std::vector<double> platform_powers;
|
||||
for (const auto& row : rows) {
|
||||
if (row.wind_speed >= params.rated_wind_speed) {
|
||||
if (row.wind_speed >= high_wind_start) {
|
||||
platform_powers.push_back(row.active_power);
|
||||
}
|
||||
}
|
||||
@@ -786,7 +810,8 @@ std::vector<ValidRow> FilterHighWindLowPower(const std::vector<ValidRow>& rows,
|
||||
std::vector<ValidRow> result;
|
||||
result.reserve(rows.size());
|
||||
for (const auto& row : rows) {
|
||||
if (row.wind_speed >= params.rated_wind_speed &&
|
||||
if (row.wind_speed >= high_wind_start &&
|
||||
!IsRatedTransitionCorner(row, params) &&
|
||||
row.active_power < lower_limit) {
|
||||
++removed_count;
|
||||
removed_points.push_back(RemovedPoint{row, "high_wind_low_power"});
|
||||
@@ -846,14 +871,26 @@ std::vector<ValidRow> FilterCurveResidualOutliers(const std::vector<ValidRow>& r
|
||||
const double median = Quantile(residuals, 0.5);
|
||||
const double mad_sigma = MedianAbsoluteDeviation(residuals, median) * 1.4826;
|
||||
const double rated_power = params.rated_power > 0.0 ? params.rated_power : options.rated_power;
|
||||
const double lower_margin = std::max(4.0 * mad_sigma, rated_power * 0.10);
|
||||
const double upper_margin = std::max(4.0 * mad_sigma, rated_power * 0.16);
|
||||
double transition_multiplier = 1.0;
|
||||
if (params.rated_wind_speed > 0.0) {
|
||||
const double interval_center = (start + end) / 2.0;
|
||||
if (interval_center >= params.rated_wind_speed - 1.0 &&
|
||||
interval_center <= params.rated_wind_speed + 1.5) {
|
||||
transition_multiplier = 1.8;
|
||||
}
|
||||
}
|
||||
const double lower_margin =
|
||||
std::max(4.0 * mad_sigma, rated_power * 0.10) * transition_multiplier;
|
||||
const double upper_margin =
|
||||
std::max(4.0 * mad_sigma, rated_power * 0.16) * transition_multiplier;
|
||||
const double lower = median - lower_margin;
|
||||
const double upper = median + upper_margin;
|
||||
|
||||
for (size_t i = 0; i < interval_rows.size(); ++i) {
|
||||
if (residuals[i] >= lower && residuals[i] <= upper) {
|
||||
result.push_back(interval_rows[i]);
|
||||
} else if (IsRatedTransitionCorner(interval_rows[i], params)) {
|
||||
result.push_back(interval_rows[i]);
|
||||
} else {
|
||||
++removed_count;
|
||||
removed_points.push_back(RemovedPoint{interval_rows[i], "curve_residual_outlier"});
|
||||
@@ -873,7 +910,7 @@ std::vector<ValidRow> FilterStrictRatedPlateau(const std::vector<ValidRow>& rows
|
||||
return rows;
|
||||
}
|
||||
|
||||
const double strict_start_wind_speed = params.rated_wind_speed + 0.5;
|
||||
const double strict_start_wind_speed = params.rated_wind_speed + 1.5;
|
||||
std::vector<double> plateau_powers;
|
||||
for (const auto& row : rows) {
|
||||
if (row.wind_speed >= strict_start_wind_speed) {
|
||||
@@ -894,6 +931,7 @@ std::vector<ValidRow> FilterStrictRatedPlateau(const std::vector<ValidRow>& rows
|
||||
result.reserve(rows.size());
|
||||
for (const auto& row : rows) {
|
||||
if (row.wind_speed >= strict_start_wind_speed &&
|
||||
!IsRatedTransitionCorner(row, params) &&
|
||||
row.active_power < lower_limit) {
|
||||
++removed_count;
|
||||
removed_points.push_back(RemovedPoint{row, "rated_plateau_low_power"});
|
||||
@@ -1270,7 +1308,14 @@ void WindPowerController::FinishJob(
|
||||
fan_removed_points);
|
||||
scheme_two_grid_speed_low_count += removed;
|
||||
|
||||
fan_rows = FilterLimitPower(fan_rows, options, removed, fan_removed_points);
|
||||
const auto initial_estimated_params = EstimateRatedParams(fan_rows, options);
|
||||
|
||||
fan_rows = FilterLimitPower(
|
||||
fan_rows,
|
||||
options,
|
||||
&initial_estimated_params,
|
||||
removed,
|
||||
fan_removed_points);
|
||||
limit_power_count += removed;
|
||||
|
||||
for (auto& row : fan_rows) {
|
||||
@@ -1293,7 +1338,8 @@ void WindPowerController::FinishJob(
|
||||
[](const ValidRow& row) { return row.active_power; },
|
||||
removed,
|
||||
fan_removed_points,
|
||||
"speed_power_outlier");
|
||||
"speed_power_outlier",
|
||||
&initial_estimated_params);
|
||||
speed_power_outlier_count += removed;
|
||||
|
||||
const auto fan_estimated_params = EstimateRatedParams(fan_rows, options);
|
||||
@@ -1357,8 +1403,12 @@ void WindPowerController::FinishJob(
|
||||
json fan_filtered = json::array();
|
||||
for (const auto& removed_point : fan_removed_points) {
|
||||
json point;
|
||||
point["fan_id"] = removed_point.row.fan_id;
|
||||
point["time"] = removed_point.row.time;
|
||||
point["wind_speed"] = removed_point.row.wind_speed;
|
||||
point["active_power"] = removed_point.row.active_power;
|
||||
point["generator_speed"] = removed_point.row.generator_speed;
|
||||
point["pitch_angle_average"] = removed_point.row.pitch_angle_average;
|
||||
point["reason"] = removed_point.reason;
|
||||
fan_filtered.push_back(point);
|
||||
}
|
||||
|
||||
@@ -69,6 +69,12 @@
|
||||
background: #24243a;
|
||||
}
|
||||
|
||||
.secondaryButton.active {
|
||||
border-color: #fbbf24;
|
||||
background: rgba(251, 191, 36, 0.14);
|
||||
color: #fde68a;
|
||||
}
|
||||
|
||||
.uploadButton input {
|
||||
display: none;
|
||||
}
|
||||
@@ -499,6 +505,10 @@ select:focus {
|
||||
min-height: 540px;
|
||||
}
|
||||
|
||||
.chartFrameEditing .u-over {
|
||||
cursor: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='28' height='28' viewBox='0 0 28 28'%3E%3Cpath d='M6 22l4.6-1.1L22.1 9.4l-3.5-3.5L7.1 17.4 6 22z' fill='%23fbbf24' stroke='%2310101a' stroke-width='1.6'/%3E%3Cpath d='M17.5 5.1l1.4-1.4c.8-.8 2-.8 2.8 0l2.6 2.6c.8.8.8 2 0 2.8l-1.4 1.4-5.4-5.4z' fill='%23fde68a' stroke='%2310101a' stroke-width='1.6'/%3E%3C/svg%3E") 4 24, crosshair;
|
||||
}
|
||||
|
||||
.uplotWrap {
|
||||
width: 100%;
|
||||
min-height: 540px;
|
||||
@@ -580,6 +590,23 @@ select:focus {
|
||||
border-color: #22c55e;
|
||||
}
|
||||
|
||||
.chartBrush {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: 3;
|
||||
border: 1px solid rgba(251, 191, 36, 0.95);
|
||||
border-radius: 50%;
|
||||
background: rgba(251, 191, 36, 0.08);
|
||||
box-shadow: 0 0 0 1px rgba(16, 16, 26, 0.8), 0 0 18px rgba(251, 191, 36, 0.24);
|
||||
pointer-events: none;
|
||||
will-change: transform;
|
||||
}
|
||||
|
||||
.chartBrush.dragging {
|
||||
background: rgba(251, 191, 36, 0.16);
|
||||
}
|
||||
|
||||
.chartTools {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import uPlot from 'uplot';
|
||||
import 'uplot/dist/uPlot.min.css';
|
||||
import * as XLSX from 'xlsx';
|
||||
@@ -49,6 +49,7 @@ const DESIGN_FIELD_HINTS = {
|
||||
|
||||
const CHUNK_SIZE = 4000;
|
||||
const CHART_HEIGHT = 540;
|
||||
const BRUSH_RADIUS = 16;
|
||||
const DEFAULT_SCHEME_ID = 'scheme_one';
|
||||
const SCHEME_TWO_ID = 'scheme_two';
|
||||
const DEFAULT_SCHEMES = [
|
||||
@@ -355,6 +356,23 @@ function exportValidRowsToExcel(fanId, rows) {
|
||||
);
|
||||
}
|
||||
|
||||
function createPointId(point, fanId) {
|
||||
return [
|
||||
point.fan_id || fanId || '',
|
||||
point.time || '',
|
||||
point.wind_speed ?? '',
|
||||
point.active_power ?? '',
|
||||
point.reason || '',
|
||||
].join('|');
|
||||
}
|
||||
|
||||
function withPointIds(points, fanId) {
|
||||
return (points || []).map((point) => ({
|
||||
...point,
|
||||
point_id: point.point_id || createPointId(point, fanId),
|
||||
}));
|
||||
}
|
||||
|
||||
function interpolateDesignPower(points, windSpeed) {
|
||||
if (!points.length) return null;
|
||||
if (windSpeed < points[0].wind_speed || windSpeed > points[points.length - 1].wind_speed) {
|
||||
@@ -417,8 +435,12 @@ function PowerCurveChart({
|
||||
filteredPoints,
|
||||
showFiltered,
|
||||
designPoints,
|
||||
editMode,
|
||||
onRestorePoints,
|
||||
}) {
|
||||
const frameRef = useRef(null);
|
||||
const chartRef = useRef(null);
|
||||
const [brush, setBrush] = useState({ visible: false, x: 0, y: 0, dragging: false });
|
||||
const validPoints = useMemo(
|
||||
() => (points || []).filter((point) => point.sample_count > 0),
|
||||
[points],
|
||||
@@ -476,7 +498,7 @@ function PowerCurveChart({
|
||||
width,
|
||||
height: CHART_HEIGHT,
|
||||
cursor: {
|
||||
drag: { x: true, y: true },
|
||||
drag: { x: !editMode, y: !editMode },
|
||||
},
|
||||
scales: {
|
||||
x: { time: false },
|
||||
@@ -546,21 +568,116 @@ function PowerCurveChart({
|
||||
},
|
||||
}, [curveX, actualY, designY], chartRef.current);
|
||||
|
||||
return () => chart.destroy();
|
||||
}, [validPoints, validScatter, validFiltered, validDesign, showFiltered]);
|
||||
const hideBrush = () => {
|
||||
setBrush((prev) => ({ ...prev, visible: false, dragging: false }));
|
||||
};
|
||||
|
||||
if (!editMode) {
|
||||
return () => chart.destroy();
|
||||
}
|
||||
|
||||
let dragging = false;
|
||||
const updateBrush = (event, nextDragging = dragging) => {
|
||||
if (!frameRef.current) return;
|
||||
const rect = frameRef.current.getBoundingClientRect();
|
||||
setBrush({
|
||||
visible: true,
|
||||
x: event.clientX - rect.left,
|
||||
y: event.clientY - rect.top,
|
||||
dragging: nextDragging,
|
||||
});
|
||||
};
|
||||
const restoreAt = (event) => {
|
||||
if (!chart.over || !onRestorePoints) return;
|
||||
const rect = chart.over.getBoundingClientRect();
|
||||
const x = event.clientX - rect.left;
|
||||
const y = event.clientY - rect.top;
|
||||
const radiusSquared = BRUSH_RADIUS * BRUSH_RADIUS;
|
||||
const restored = [];
|
||||
for (const point of validFiltered) {
|
||||
const pointX = chart.valToPos(point.wind_speed, 'x');
|
||||
const pointY = chart.valToPos(point.active_power, 'y');
|
||||
if (!Number.isFinite(pointX) || !Number.isFinite(pointY)) {
|
||||
continue;
|
||||
}
|
||||
const dx = pointX - x;
|
||||
const dy = pointY - y;
|
||||
if (dx * dx + dy * dy <= radiusSquared) {
|
||||
restored.push(point);
|
||||
}
|
||||
}
|
||||
if (restored.length) {
|
||||
onRestorePoints(restored);
|
||||
}
|
||||
};
|
||||
const handlePointerDown = (event) => {
|
||||
if (event.button !== 0) return;
|
||||
event.preventDefault();
|
||||
dragging = true;
|
||||
updateBrush(event, true);
|
||||
restoreAt(event);
|
||||
};
|
||||
const handlePointerMove = (event) => {
|
||||
updateBrush(event);
|
||||
if (dragging) {
|
||||
restoreAt(event);
|
||||
}
|
||||
};
|
||||
const handlePointerUp = (event) => {
|
||||
updateBrush(event, false);
|
||||
dragging = false;
|
||||
};
|
||||
|
||||
chart.over.addEventListener('pointerdown', handlePointerDown);
|
||||
chart.over.addEventListener('pointermove', handlePointerMove);
|
||||
chart.over.addEventListener('pointerleave', hideBrush);
|
||||
window.addEventListener('pointerup', handlePointerUp);
|
||||
|
||||
return () => {
|
||||
chart.over.removeEventListener('pointerdown', handlePointerDown);
|
||||
chart.over.removeEventListener('pointermove', handlePointerMove);
|
||||
chart.over.removeEventListener('pointerleave', hideBrush);
|
||||
window.removeEventListener('pointerup', handlePointerUp);
|
||||
chart.destroy();
|
||||
};
|
||||
}, [
|
||||
validPoints,
|
||||
validScatter,
|
||||
validFiltered,
|
||||
validDesign,
|
||||
showFiltered,
|
||||
editMode,
|
||||
onRestorePoints,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!editMode) {
|
||||
setBrush((prev) => ({ ...prev, visible: false, dragging: false }));
|
||||
}
|
||||
}, [editMode]);
|
||||
|
||||
if (!validPoints.length) {
|
||||
return <div className="emptyChart">暂无可绘制的曲线数据</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="chartFrame">
|
||||
<div className={`chartFrame ${editMode ? 'chartFrameEditing' : ''}`} ref={frameRef}>
|
||||
<div className="chartLegend">
|
||||
<span><i className="legendDot scatter" />散点数据</span>
|
||||
<span><i className="legendLine actual" />实际功率</span>
|
||||
{validDesign.length > 0 && <span><i className="legendLine design" />设计功率</span>}
|
||||
</div>
|
||||
<div className="uplotWrap" ref={chartRef} />
|
||||
{editMode && brush.visible && (
|
||||
<div
|
||||
className={`chartBrush ${brush.dragging ? 'dragging' : ''}`}
|
||||
style={{
|
||||
width: BRUSH_RADIUS * 2,
|
||||
height: BRUSH_RADIUS * 2,
|
||||
transform: `translate(${brush.x - BRUSH_RADIUS}px, ${brush.y - BRUSH_RADIUS}px)`,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -575,6 +692,8 @@ export default function HomePage() {
|
||||
const [result, setResult] = useState(null);
|
||||
const [selectedFan, setSelectedFan] = useState('');
|
||||
const [showFiltered, setShowFiltered] = useState(false);
|
||||
const [isEditMode, setIsEditMode] = useState(false);
|
||||
const [restoredPointIds, setRestoredPointIds] = useState({});
|
||||
const [readingDesign, setReadingDesign] = useState(false);
|
||||
const [designRows, setDesignRows] = useState(() => createDesignRows());
|
||||
const [designMeta, setDesignMeta] = useState(null);
|
||||
@@ -594,13 +713,63 @@ export default function HomePage() {
|
||||
const selectedScatter = selectedFan && result?.scatter_points
|
||||
? result.scatter_points[selectedFan] || []
|
||||
: [];
|
||||
const selectedFiltered = selectedFan && result?.filtered_points
|
||||
const selectedFilteredRaw = selectedFan && result?.filtered_points
|
||||
? result.filtered_points[selectedFan] || []
|
||||
: [];
|
||||
const selectedFiltered = useMemo(
|
||||
() => withPointIds(selectedFilteredRaw, selectedFan),
|
||||
[selectedFilteredRaw, selectedFan],
|
||||
);
|
||||
const selectedRestoredIdSet = useMemo(
|
||||
() => new Set(restoredPointIds[selectedFan] || []),
|
||||
[restoredPointIds, selectedFan],
|
||||
);
|
||||
const restoredFilteredPoints = useMemo(
|
||||
() => selectedFiltered.filter((point) => selectedRestoredIdSet.has(point.point_id)),
|
||||
[selectedFiltered, selectedRestoredIdSet],
|
||||
);
|
||||
const visibleFilteredPoints = useMemo(
|
||||
() => selectedFiltered.filter((point) => !selectedRestoredIdSet.has(point.point_id)),
|
||||
[selectedFiltered, selectedRestoredIdSet],
|
||||
);
|
||||
const effectiveScatter = useMemo(
|
||||
() => [...selectedScatter, ...restoredFilteredPoints],
|
||||
[selectedScatter, restoredFilteredPoints],
|
||||
);
|
||||
const selectedEstimatedParams = selectedFan && result?.estimated_params
|
||||
? result.estimated_params[selectedFan]
|
||||
: null;
|
||||
|
||||
const handleRestorePoints = useCallback((pointsToRestore) => {
|
||||
if (!selectedFan || !pointsToRestore.length) return;
|
||||
setRestoredPointIds((prev) => {
|
||||
const nextIds = new Set(prev[selectedFan] || []);
|
||||
let changed = false;
|
||||
for (const point of pointsToRestore) {
|
||||
const pointId = point.point_id || createPointId(point, selectedFan);
|
||||
if (!nextIds.has(pointId)) {
|
||||
nextIds.add(pointId);
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
if (!changed) return prev;
|
||||
return {
|
||||
...prev,
|
||||
[selectedFan]: Array.from(nextIds),
|
||||
};
|
||||
});
|
||||
}, [selectedFan]);
|
||||
|
||||
const handleToggleEditMode = useCallback(() => {
|
||||
setIsEditMode((prev) => {
|
||||
const next = !prev;
|
||||
if (next) {
|
||||
setShowFiltered(true);
|
||||
}
|
||||
return next;
|
||||
});
|
||||
}, []);
|
||||
|
||||
const totalRows = useMemo(
|
||||
() => files.reduce((sum, file) => sum + file.row_count, 0),
|
||||
[files],
|
||||
@@ -726,6 +895,8 @@ export default function HomePage() {
|
||||
setResult(null);
|
||||
setSelectedFan('');
|
||||
setShowFiltered(false);
|
||||
setIsEditMode(false);
|
||||
setRestoredPointIds({});
|
||||
try {
|
||||
const parsedFiles = [];
|
||||
for (const file of selectedFiles) {
|
||||
@@ -812,6 +983,8 @@ export default function HomePage() {
|
||||
setError('');
|
||||
setResult(null);
|
||||
setShowFiltered(false);
|
||||
setIsEditMode(false);
|
||||
setRestoredPointIds({});
|
||||
setProgress('正在准备标准化数据');
|
||||
|
||||
try {
|
||||
@@ -1181,22 +1354,30 @@ export default function HomePage() {
|
||||
<h2>功率曲线</h2>
|
||||
<div className="chartTools">
|
||||
<span>
|
||||
曲线 {selectedPoints.length} 点 · 散点 {selectedScatter.length.toLocaleString()} 点
|
||||
· 滤除 {selectedFiltered.length.toLocaleString()} 点
|
||||
曲线 {selectedPoints.length} 点 · 散点 {effectiveScatter.length.toLocaleString()} 点
|
||||
· 滤除 {visibleFilteredPoints.length.toLocaleString()} 点
|
||||
</span>
|
||||
<button
|
||||
className="secondaryButton"
|
||||
type="button"
|
||||
onClick={() => setShowFiltered((prev) => !prev)}
|
||||
disabled={!selectedFiltered.length}
|
||||
disabled={isEditMode || !visibleFilteredPoints.length}
|
||||
>
|
||||
{showFiltered ? '隐藏滤除点' : '显示滤除点'}
|
||||
</button>
|
||||
<button
|
||||
className={`secondaryButton ${isEditMode ? 'active' : ''}`}
|
||||
type="button"
|
||||
onClick={handleToggleEditMode}
|
||||
disabled={!selectedFiltered.length && !isEditMode}
|
||||
>
|
||||
{isEditMode ? '退出编辑' : '进入编辑'}
|
||||
</button>
|
||||
<button
|
||||
className="secondaryButton"
|
||||
type="button"
|
||||
onClick={() => exportValidRowsToExcel(selectedFan, selectedScatter)}
|
||||
disabled={!selectedScatter.length}
|
||||
onClick={() => exportValidRowsToExcel(selectedFan, effectiveScatter)}
|
||||
disabled={!effectiveScatter.length}
|
||||
>
|
||||
导出有效数据
|
||||
</button>
|
||||
@@ -1228,10 +1409,12 @@ export default function HomePage() {
|
||||
)}
|
||||
<PowerCurveChart
|
||||
points={selectedPoints}
|
||||
scatterPoints={selectedScatter}
|
||||
filteredPoints={selectedFiltered}
|
||||
showFiltered={showFiltered}
|
||||
scatterPoints={effectiveScatter}
|
||||
filteredPoints={visibleFilteredPoints}
|
||||
showFiltered={showFiltered || isEditMode}
|
||||
designPoints={designCurve}
|
||||
editMode={isEditMode}
|
||||
onRestorePoints={handleRestorePoints}
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user