16 KiB
数据集详情「二维图表」返工实现计划 v2(QwtPlot)
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。
步骤 2:QwtPlot 接入 + 交互(治 #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。
步骤 4:ColorMapService + 抓原版真实色值(治 #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。
- 散点页:抓 Plotly
Files: ColorMapService.hpp/.cpp;测试 tests/app/test_colormap_service.cpp。
- Step 4.2 写失败测试(对数映射):
#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(离散)。 - 等值线:每条
ContourLinedrawPolyline(黑、细);受「显示等值线标注」控制在线中段绘制旋转贴线的数值文本(#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 「简化容差」滑块(0–1)→ 改
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),功能实现后续单独立项。