docs(plan): 二维图表返工实现计划 v2(QwtPlot 三层分离,9步对照验收)

This commit is contained in:
gaozheng 2026-06-11 14:54:57 +08:00
parent b772b5a169
commit e0c36e3600
1 changed files with 205 additions and 0 deletions

View File

@ -0,0 +1,205 @@
# 数据集详情「二维图表」返工实现计划 v2QwtPlot
> **For agentic workers:** REQUIRED SUB-SKILL: 用 superpowers:subagent-driven-development 或 executing-plans 执行。步骤用 `- [ ]`
> **铁律(来自返工方案):** 每一步做完**必须**与原版对照验收,通过再进下一步——不许一次性做完才看。任何对原版的不确定,**必须用 Playwright 实地学习原版**,禁止联想猜测。
**Goal:** 推翻当前 QGraphicsView 二维图表按《Geopro3.0_二维图表返工技术方案.md》用 **QwtPlot坐标系/交互/图例三层分离)+ VTK 算法(等值线多边形)+ 连续对数色阶** 重做「原数据散点图」与「网格数据等值线图」,修复用户列出的 9 个问题,达到**视觉等价 + 交互一致**(非字节级像素)。
**Architecture:** 三层分离——工具条(属于面板)/ QwtPlot数据区 + 固定坐标轴 + Magnifier/Panner 交互)/ 独立 ColorBarWidget不入数据坐标系。等值线多边形复用已有 `ContourBands`(VTK 算法);色阶用 `QwtLinearColorMap` 连续插值 + 对数标度图与图例同源ColorMapService
**Tech Stack:** C++17、Qt6 Widgets、**Qwt 6.2(已集成,见 cmake/qwt.cmake目标名 `qwt`,头在 external/qwt-src/src**、VTK 9.6仅算法、GoogleTest/CTest。
**依据:** `docs/Geopro3.0_二维图表返工技术方案.md`(架构权威)、`docs/superpowers/specs/2026-06-11-dataset-detail-view-design.md`(数据/接口)。
---
## 0. 复用与替换盘点(不要重造已有的)
**复用(不动):**
- `src/data/api/ApiDatasetRepository.*`、`src/data/dto/DatasetChartDto.*`、`src/controller/DatasetDetailController.*`(数据层+编排,`ChartData` 已带 scatter/grid/colorScale×2/anomalies
- `src/render/ContourBands.*`VTK 算分层多边形+等值线几何,作为等值线图的几何来源 = 方案的 ContourEngine
- `src/core/model/{Field,ColorScale,Anomaly}.hpp`、`src/app/panels/AnomalyTablePanel.*`(异常表,列对,复用)。
- 既有下划线页签组件 `buildTabbedPanel`main.cpp 中「异常/对象属性」面板用的同款§步骤1 复用)。
**替换(删除/重写):**
- ❌ `src/app/panels/chart/DatasetChartView.*`(裸 QGraphicsView—— 删除。
- ❌ `src/app/panels/DatasetDetailPage.*`(旧两按钮+图表)—— 重写为基于 QwtPlot 的视图。
- `src/app/panels/DatasetDetailPanel.*`、`main.cpp` 接线 —— 按新组件调整。
---
## 文件结构(新建/修改)
| 文件 | 职责 |
|---|---|
| `src/app/panels/chart/ColorMapService.hpp/.cpp`(建) | 色阶服务:断点+颜色、连续对数映射、产出 `QwtLinearColorMap` 与离散色带;图/图例同源 |
| `src/app/panels/chart/ColorBarWidget.hpp/.cpp`(建) | 独立色阶条 QWidget固定不入数据坐标系 |
| `src/app/panels/chart/ScatterPlotItem.hpp/.cpp`(建) | QwtPlotItem散点方块按连续色阶着色 |
| `src/app/panels/chart/ContourPlotItem.hpp/.cpp`(建) | QwtPlotItem画 ContourBands 分层填充多边形 + 等值线 + 标注 + 异常叠加 |
| `src/app/panels/chart/RawDataChartView.hpp/.cpp`(建) | 原数据页:工具条 + QwtPlot + ScatterPlotItem + ColorBarWidget |
| `src/app/panels/chart/GridDataChartView.hpp/.cpp`(建) | 网格数据页:工具条 + QwtPlot + ContourPlotItem + ColorBarWidget |
| `src/app/panels/DatasetDetailPage.hpp/.cpp`(重写) | 单 ds 页:下划线页签(原数据/网格数据) + 两视图 + 下方(异常列表/描述) |
| `src/app/panels/DatasetDetailPanel.hpp/.cpp`(改) | 多 Tab 壳(保留 dsId 去重/反向联动) |
| `src/app/panels/DescriptionPanel.hpp/.cpp`(建) | 下方「描述」页(纯文本/只读富文本占位) |
| `src/app/panels/chart/DatasetChartView.*`(删) | 旧 QGraphicsView 渲染器 |
| `src/app/CMakeLists.txt`(改) | 注册新文件 + 链接 `qwt`;移除 DatasetChartView |
| 测试 | ColorMapService 对数映射 / ContourBands 既有 / 散点着色 单测 |
**构建/测试命令(本机工具链):**
- 构建:`powershell.exe -ExecutionPolicy Bypass -File scripts/dev-build.ps1`
- 测试:`powershell.exe -ExecutionPolicy Bypass -File scripts/dev-test.ps1`
- 运行(视觉验收):`build/release/src/app/geopro_desktop.exe`(需登录)
**Qwt 关键类参考**(实现时可查 `external/qwt-src/src/` 头文件确认签名):`QwtPlot`、`QwtPlotItem`、`QwtPlotMagnifier`、`QwtPlotPanner`、`QwtPlotZoomer`、`QwtLinearColorMap`、`QwtScaleEngine`/`QwtLogScaleEngine`、`QwtText`、`QwtPlotCanvas`。
---
## 步骤 1三层骨架 + UI 结构(治 #6 #7 #8
**目标**:先把布局结构做对,不画图。下划线页签、工具条归位、下方双页签。
**研究原版Playwright**:对照 `assets/web-datasetinfo-*.png` 与实测——确认 (a) 原数据/网格数据 = 下划线页签(非按钮);(b) 两个页签各自的工具条项§方案7(c) 下方 = 异常列表 + 描述 双页签。
**Files:** 重写 `DatasetDetailPage.*`;建 `DescriptionPanel.*`;改 `DatasetDetailPanel.*`、`src/app/CMakeLists.txt`。
- [ ] **Step 1.1** 先研究:用 Playwright 打开 `datasetInfo?id=1439812897218560&ddCode=dd_inversion_data`,截图原数据/网格两页签的工具条与下方区域,确认结构(不靠记忆)。
- [ ] **Step 1.2** 复用 `buildTabbedPanel`main.cpp 中「异常/对象属性」用的下划线页签)做 `DatasetDetailPage` 顶部「原数据/网格数据」切换;内容区先放两个空 `QWidget` 占位RawDataChartView/GridDataChartView 后续填)。
- [ ] **Step 1.3** 下方用同款 `buildTabbedPanel` 做「异常列表(现有 `AnomalyTablePanel`/ 描述(新 `DescriptionPanel`,先放只读文本占位)」。
- [ ] **Step 1.4** 工具条:原数据页工具条 = [网格][色阶配置][当前图形▼][另存为];网格数据页工具条 = [网格][色阶配置][白化][滤波处理][☑显示异常][☑显示等值线标注][☐显示等值线提示信息][简化容差滑块][异常标注][自动标注][另存为]。**先只放按钮/控件占位(无功能)**,位置在各自页签视图内部顶部。
- [ ] **Step 1.5** 构建通过;启动 app 双击 ds**对照原版验收**:页签是下划线样式、工具条在各自页签内、下方有两个页签。截图发用户确认后再进步骤 2。
---
## 步骤 2QwtPlot 接入 + 交互(治 #3 #4 #9
**研究原版Playwright关键——消除「四象限」歧义**
- [ ] **Step 2.1** 在原版散点页拖动/缩放,观察:坐标轴是否固定、刻度值是否跟随、能否平移到数据范围外(负区)。抓 Plotly `layout.xaxis/yaxis`range/scaleanchor/autorange/side。**判定原数据到底是「真四象限」还是「单象限+自由平移」**,网格数据同样观察。把结论记入本步骤再实现,不靠猜。
**Files:** `RawDataChartView.*`、`GridDataChartView.*`(先只含 QwtPlot 空图 + 交互);`src/app/CMakeLists.txt` 链 `qwt`
- [ ] **Step 2.2** 两视图各内嵌一个 `QwtPlot`。按步骤2.1结论设轴:网格数据 = 单象限x/y 从数据 min原点左下原数据 = 按结论(四象限则轴范围含负、十字交叉;单象限则同网格)。
- [ ] **Step 2.3**`QwtPlotMagnifier(plot->canvas())`(滚轮缩放)+ `QwtPlotPanner(plot->canvas())`(拖动平移)。确认:**轴位置不动、只有数据区平移缩放、刻度数值自动跟随**QwtPlot 默认行为)。
- [ ] **Step 2.4** 纵横比(#9原版剖面"宽扁"。先用 `plot->setAxisScale` 固定 y 数据范围、x 自适应填充宽度;若需等比例尺,在 resize/replot 时按数据 x/y 跨度调 canvas 宽高(方案 §4.4)。
- [ ] **Step 2.5** 构建+运行,**对照原版验收**:拖动只动数据、不花屏、轴值跟随、宽扁比例接近。截图确认后进步骤 3。
---
## 步骤 3独立色阶条 ColorBarWidget#1
**Files:** `ColorBarWidget.*`;放进 Raw/GridDataChartView 布局中 QwtPlot **下方**(兄弟 widget
- [ ] **Step 3.1** `ColorBarWidget : QWidget``paintEvent` 自绘一条水平色带(暂用占位色阶)+ 刻度数值。固定高度(~36px色条+文字)。
- [ ] **Step 3.2** 布局:`QVBoxLayout`= [工具条][QwtPlot stretch][ColorBarWidget 固定高]。**ColorBar 不入 QwtPlot 坐标系**。
- [ ] **Step 3.3** 运行验收:缩放/拖动主图时色阶条**不动、不与横轴重叠**(治 #1)。截图确认进步骤 4。
---
## 步骤 4ColorMapService + 抓原版真实色值(治 #2 基础)
**研究原版Playwright关键**
- [ ] **Step 4.1** 抓原版色值与映射方式(**严禁凭记忆配色**
- 散点页:抓 Plotly `data[0].marker.colorscale`[[pos,color],…])、`cmin/cmax`、是否连续;判断标度(线性 vs 对数)——对色阶条断点 `17.105,25.937,…` 做比值检验确认对数/等比。
- 网格页:色阶来自 `colorGradation/getDetail`(已知 `properties.colorBar=[[值,rgba],…]`、type1 原数据/type2 网格)。
- 取色兜底:对色阶条截图每格中心取 RGB。
**Files:** `ColorMapService.hpp/.cpp`;测试 `tests/app/test_colormap_service.cpp`
- [ ] **Step 4.2 写失败测试**(对数映射):
```cpp
#include <gtest/gtest.h>
#include "panels/chart/ColorMapService.hpp"
using geopro::app::ColorMapService;
TEST(ColorMapService, LogMapsValueToUnitInterval) {
ColorMapService s;
s.setRange(10.0, 1000.0, /*logScale=*/true);
EXPECT_NEAR(s.normalized(10.0), 0.0, 1e-9);
EXPECT_NEAR(s.normalized(1000.0), 1.0, 1e-9);
EXPECT_NEAR(s.normalized(100.0), 0.5, 1e-9); // log10 中点
}
```
- [ ] **Step 4.3** 注册测试到 `tests/CMakeLists.txt`app 段,已 `target_include_directories ... src/app`);跑确认失败(编译失败)。
- [ ] **Step 4.4 实现** `ColorMapService`
- 持 `core::ColorScale`(离散断点+颜色,来自 colorBar+ `vmin/vmax/logScale`
- `double normalized(double v)`log 时 `(log(v)-log(vmin))/(log(vmax)-log(vmin))`否则线性clamp [0,1]。
- `QwtLinearColorMap* makeQwtColorMap()`:用断点的归一化位置 + 颜色 `addColorStop`,连续插值(默认 ScaledColors
- `core::Rgba colorAt(double v)`:连续插值取色(散点用);以及离散 `bandColor`(等值线用)。
- 图与图例ColorBarWidget+ 等值线图都用**同一个 ColorMapService 实例**。
- [ ] **Step 4.5** 跑测试通过;提交。
---
## 步骤 5原数据散点图#2
**研究原版Playwright**:确认散点 = 方块 symbol、白描边、按 vlist 连续着色、x:y 关系步骤2结论
**Files:** `ScatterPlotItem.*`;接入 `RawDataChartView`
- [ ] **Step 5.1** `ScatterPlotItem : public QwtPlotItem`,重写 `draw(QPainter*, xMap, yMap, canvasRect)`:对每个点 `(xlist[i],ylist[i])``xMap.transform/yMap.transform` 映射到像素,画固定像素边长的方块(白描边 1px填色 = `ColorMapService::colorAt(vlist[i])`。`rtti()` 返回自定义。
- [ ] **Step 5.2** `RawDataChartView::setData(scatter, colorMapService)`:建 ScatterPlotItem attach 到 QwtPlot设轴范围为数据包围盒`replot()`ColorBarWidget 用同一 service 刷新。
- [ ] **Step 5.3** 运行,**与原版散点并排对照配色**#2颜色应连续插值、与原版一致。截图确认进步骤 6。
---
## 步骤 6网格数据等值线图#2 #5
**研究原版Playwright**:确认填充色带(离散 banded+ 黑色等值线 + **线上数值标注**(贴线方向)+ 数据凸包白边。
**Files:** `ContourPlotItem.*`(复用 `ContourBands`);接入 `GridDataChartView`
- [ ] **Step 6.1** `ContourPlotItem : QwtPlotItem`,持 `ContourBandsResult`(来自 `geopro::render::buildContourBands(grid, colorScale, opt)`)。`draw()`
- 填充:每个 `BandPolygon` 用 xMap/yMap 把局部坐标映射到像素,`painter->drawPolygon`,填 `band.color`(离散)。
- 等值线:每条 `ContourLine` `drawPolyline`(黑、细);受「显示等值线标注」控制在线中段绘制旋转贴线的数值文本(#5
- [ ] **Step 6.2** 异常叠加:把 ds 异常(`core::Anomaly`,局部坐标)按 markType 画在同一 item 之上(虚线多段线/多边形/点),受「显示异常」控制。
- [ ] **Step 6.3** `GridDataChartView::setData(grid, gridColorMapService, anomalies)`:构建 ContourPlotItem attach轴=数据范围ColorBar 用网格色阶type2
- [ ] **Step 6.4** 运行,**与原版网格图并排对照**#2 配色、#5 标注、凸包白边、宽扁比例)。截图确认进步骤 7。
---
## 步骤 7工具条联动显示开关 + 简化容差)
- [ ] **Step 7.1** 「显示异常 / 显示等值线标注 / 显示等值线提示信息」复选框 → 切 ContourPlotItem 对应开关 + replot。
- [ ] **Step 7.2** 「简化容差」滑块01→ 改 `ContourOptions::simplifyTol` 重算 ContourBands + replot对应原版滑块
- [ ] **Step 7.3** 「白化 / 滤波处理 / 网格 / 色阶配置 / 异常标注 / 自动标注 / 另存为」**先做占位按钮**(弹「待实现」或留空槽),功能后续单独立项。运行验收开关行为与原版一致。
---
## 步骤 8接线 DatasetDetailController + main.cpp替换旧渲染
- [ ] **Step 8.1** `DatasetDetailPage::setData(ChartData)`:分别喂 `RawDataChartView`scatter + scatterColorScale`GridDataChartView`grid + gridColorScale + anomalies异常列表喂 `AnomalyTablePanel`
- [ ] **Step 8.2** `main.cpp`:删除对旧 `DatasetChartView`/`DatasetDetailPage` 的引用;`DatasetDetailPanel` 改用新 `DatasetDetailPage`。`chartReady`→`openOrUpdate` 链路不变。删除 `src/app/panels/chart/DatasetChartView.*` 及其 CMake 注册。
- [ ] **Step 8.3** 全量构建 + `dev-test.ps1` 不回归;启动 app 双击 ds **整体对照验收**
---
## 步骤 9整体对照验收治全部 9 项)
- [ ] **Step 9.1** 原数据、网格数据分别与原版并排截图,逐条核对方案 §10 验收清单:配色/轴固定/色阶条固定/标注/坐标系象限/宽扁比例/拖动流畅/页签样式/工具条归位/下方双页签。
- [ ] **Step 9.2** cpp-reviewer 审查本次改动;处理 CRITICAL/HIGH。
- [ ] **Step 9.3** 收尾提交。
---
## 验收标准(方案 §10视觉等价 + 交互一致,非字节级)
- [ ] 配色与原版视觉一致(连续插值、对数分布、同色阶)
- [ ] 轴固定、拖动/缩放只动数据区、刻度值跟随、不花屏
- [ ] 色阶条固定轴下方、不随图缩放、不与轴重叠
- [ ] 等值线数值标注显示、贴线方向
- [ ] 原数据/网格数据坐标系象限与缩放行为分别与原版一致
- [ ] 宽扁比例与原版一致
- [ ] 页签下划线样式、工具条各自页签内、下方异常列表+描述双页签
- [ ] 全量测试不回归
- ✗(明确不作为标准):与 web 逐像素 diff 相同
---
## 自审Spec/方案 覆盖核对)
- 9 个问题逐条 → 步骤 1(#6/#7/#8)、2(#3/#4/#9)、3(#1)、4-5(#2)、6(#2/#5) 全覆盖。✓
- 复用项ContourBands/数据层/AnomalyTablePanel/buildTabbedPanel已标注不重造。✓
- 替换项DatasetChartView/旧 DatasetDetailPage明确删除。✓
- 每步含「Playwright 研究原版 + 对照验收」检查点(铁律)。✓
- 可测部分ColorMapService 对数映射)有 TDD视觉部分以对照验收为准方案明确视觉等价非像素级。✓
- 待澄清/研究项(步骤 2.1 四象限歧义、步骤 4.1 真实色值与标度)已显式列为开工先研究项,不靠猜。✓
> 工具功能(白化/滤波/自动标注/网格化/色阶配置/另存为/导出)本计划只做**占位归位**(治 #7),功能实现后续单独立项。