From facb812bca8e0d32e84db6afd2ae3979f2f0a433 Mon Sep 17 00:00:00 2001 From: gaozheng Date: Thu, 11 Jun 2026 11:57:49 +0800 Subject: [PATCH] =?UTF-8?q?feat(ui):=20DatasetChartView=20=E6=95=A3?= =?UTF-8?q?=E7=82=B9/=E7=AD=89=E5=80=BC=E9=9D=A2/=E5=BC=82=E5=B8=B8?= =?UTF-8?q?=E5=8F=A0=E5=8A=A0(QGraphicsView)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/CMakeLists.txt | 1 + src/app/panels/chart/DatasetChartView.cpp | 216 ++++++++++++++++++++++ src/app/panels/chart/DatasetChartView.hpp | 38 ++++ 3 files changed, 255 insertions(+) create mode 100644 src/app/panels/chart/DatasetChartView.cpp create mode 100644 src/app/panels/chart/DatasetChartView.hpp diff --git a/src/app/CMakeLists.txt b/src/app/CMakeLists.txt index db8ac21..2024e52 100644 --- a/src/app/CMakeLists.txt +++ b/src/app/CMakeLists.txt @@ -26,6 +26,7 @@ add_executable(geopro_desktop WIN32 panels/ObjectTreePanel.cpp panels/DynamicFormView.cpp panels/ObjectExceptionPanel.cpp + panels/chart/DatasetChartView.cpp CentralScene.cpp ProjectListDialog.cpp SettingsDialog.cpp) diff --git a/src/app/panels/chart/DatasetChartView.cpp b/src/app/panels/chart/DatasetChartView.cpp new file mode 100644 index 0000000..60ce1ed --- /dev/null +++ b/src/app/panels/chart/DatasetChartView.cpp @@ -0,0 +1,216 @@ +#include "panels/chart/DatasetChartView.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +namespace geopro::app { +using geopro::core::Rgba; + +static QColor toQ(const Rgba& c) { return QColor(c.r, c.g, c.b, c.a); } + +DatasetChartView::DatasetChartView(QWidget* parent) + : QGraphicsView(parent), scene_(new QGraphicsScene(this)) { + setScene(scene_); + setRenderHint(QPainter::Antialiasing, true); + setDragMode(QGraphicsView::ScrollHandDrag); + // 场景 y 向上为正:用 y 翻转的变换(QGraphicsView 默认 y 向下)。 + scale(1, -1); +} + +void DatasetChartView::clearChart() { + scene_->clear(); anomalyItems_.clear(); +} + +void DatasetChartView::showScatter(const geopro::core::ScatterField& f, const geopro::core::ColorScale& cs) { + clearChart(); + const double sz = 0.6; // 方块边长(数据单位,近似 web 方点) + for (size_t i = 0; i < f.v.size(); ++i) { + auto* r = scene_->addRect(f.x[i] - sz / 2, f.y[i] - sz / 2, sz, sz, + QPen(Qt::white, 0), QBrush(toQ(cs.colorAt(f.v[i])))); + r->setPen(QPen(Qt::white, 0)); // 白描边 + } + rebuildAnomalyItems(); + setLegend(cs); + viewport()->update(); + fitInView(scene_->itemsBoundingRect(), Qt::KeepAspectRatio); // x:y 等比 +} + +void DatasetChartView::showContour(const geopro::core::Grid& g, const geopro::core::ColorScale& cs, + const geopro::render::ContourOptions& opt) { + clearChart(); + const auto r = geopro::render::buildContourBands(g, cs, opt); + for (const auto& b : r.bands) { + QPainterPath path; if (b.ring.empty()) continue; + path.moveTo(b.ring[0].x, b.ring[0].y); + for (size_t k = 1; k < b.ring.size(); ++k) path.lineTo(b.ring[k].x, b.ring[k].y); + path.closeSubpath(); + auto* it = scene_->addPath(path, QPen(Qt::NoPen), QBrush(toQ(b.color))); + it->setZValue(0); + } + if (showContourLines_) + for (const auto& l : r.lines) { + if (l.pts.empty()) continue; + QPainterPath path; path.moveTo(l.pts[0].x, l.pts[0].y); + for (size_t k = 1; k < l.pts.size(); ++k) path.lineTo(l.pts[k].x, l.pts[k].y); + auto* it = scene_->addPath(path, QPen(QColor(0, 0, 0), 0)); + it->setZValue(1); + } + rebuildAnomalyItems(); + setLegend(cs); + viewport()->update(); + fitInView(scene_->itemsBoundingRect(), Qt::IgnoreAspectRatio); // 剖面 X/Y 各自铺满 +} + +void DatasetChartView::setAnomalies(const std::vector& list) { + anomalies_ = list; rebuildAnomalyItems(); +} +void DatasetChartView::setHiddenAnomalies(const std::set& hidden) { hidden_ = hidden; rebuildAnomalyItems(); } +void DatasetChartView::setShowAnomalies(bool on) { showAnomalies_ = on; rebuildAnomalyItems(); } +void DatasetChartView::setShowContourLines(bool on) { showContourLines_ = on; } + +void DatasetChartView::rebuildAnomalyItems() { + for (auto* it : anomalyItems_) { scene_->removeItem(it); delete it; } + anomalyItems_.clear(); + if (!showAnomalies_) return; + for (int i = 0; i < static_cast(anomalies_.size()); ++i) { + if (hidden_.count(i)) continue; + const auto& a = anomalies_[i]; + if (a.localPts.size() < 2) continue; + QPainterPath path; path.moveTo(a.localPts[0].x, a.localPts[0].y); + for (size_t k = 1; k < a.localPts.size(); ++k) path.lineTo(a.localPts[k].x, a.localPts[k].y); + if (static_cast(a.markType) == 3) path.closeSubpath(); + QPen pen(QColor(QString::fromStdString(a.lineColor)), 0); + if (a.dashed) pen.setStyle(Qt::DashLine); + auto* it = scene_->addPath(path, pen); + it->setZValue(2); + anomalyItems_.push_back(it); + } +} + +void DatasetChartView::wheelEvent(QWheelEvent* e) { + const double f = e->angleDelta().y() > 0 ? 1.15 : 1 / 1.15; + scale(f, f); +} + +// Task 3.2:坐标轴 overlay + 底部色阶图例。 +// drawForeground 在每次 repaint 时调用(缩放/平移后自动重绘),无需手动更新。 +// 轴线"钉"在 viewport 左/下边:以 viewport()->rect() 为参考, +// 用 mapToScene / mapFromScene 把场景范围映射到像素位置。 +void DatasetChartView::drawForeground(QPainter* p, const QRectF& /*rect*/) { + if (legendScale_.empty()) return; + + p->save(); + // 重置变换,后续全用视口像素坐标绘制 + p->resetTransform(); + + const QRect vr = viewport()->rect(); + const int W = vr.width(); + const int H = vr.height(); + + // ── 色阶图例(底部)──────────────────────────────── + const int legendH = 16; // 色条高度(px) + const int legendMarginB = 20; // 底部文字留白 + const int legendMarginL = 50; // 左侧轴标签留白 + const int legendMarginR = 10; + const int legendY = H - legendMarginB - legendH; + const int legendW = W - legendMarginL - legendMarginR; + + const std::vector stops = legendScale_.stopValues(); + if (stops.size() >= 2 && legendW > 0) { + const int nSeg = static_cast(stops.size()) - 1; + const double segW = static_cast(legendW) / nSeg; + for (int s = 0; s < nSeg; ++s) { + // 取该段中点值的颜色(与 web 阶梯色一致) + const double midVal = (stops[s] + stops[s + 1]) * 0.5; + const QColor c = toQ(legendScale_.colorAt(midVal)); + const int x0 = legendMarginL + static_cast(s * segW); + const int x1 = legendMarginL + static_cast((s + 1) * segW); + p->fillRect(QRect(x0, legendY, x1 - x0, legendH), c); + } + // 边框 + p->setPen(QPen(QColor(80, 80, 80), 1)); + p->drawRect(QRect(legendMarginL, legendY, legendW, legendH)); + + // 分段值文字(在每个分割点下方) + p->setPen(Qt::black); + QFont font = p->font(); + font.setPixelSize(10); + p->setFont(font); + for (int s = 0; s <= nSeg; ++s) { + const int xTick = legendMarginL + static_cast(s * segW); + const QString label = QString::number(stops[s], 'g', 4); + QRect textR(xTick - 20, legendY + legendH + 2, 40, 14); + p->drawText(textR, Qt::AlignCenter, label); + } + } + + // ── 坐标轴(左/下边钉住视口)──────────────────────── + // 把视口四个角映射到场景,得到场景可见范围 + const QPointF sceneTL = mapToScene(vr.topLeft()); + const QPointF sceneBR = mapToScene(vr.bottomRight()); + + // 因为 y 轴翻转(scale(1,-1)),场景坐标 y 向上为正 + // sceneTL 的 scene-y 可能大于 sceneBR 的 scene-y,需取 min/max + const double sceneXMin = qMin(sceneTL.x(), sceneBR.x()); + const double sceneXMax = qMax(sceneTL.x(), sceneBR.x()); + const double sceneYMin = qMin(sceneTL.y(), sceneBR.y()); + const double sceneYMax = qMax(sceneTL.y(), sceneBR.y()); + + const int axisLeft = legendMarginL; // 左轴 x 像素 + const int axisBottom = legendY - 4; // 下轴 y 像素(色阶条上方留 4px) + + p->setPen(QPen(QColor(100, 100, 100), 1)); + + // 左侧轴线(竖) + p->drawLine(axisLeft, 4, axisLeft, axisBottom); + // 下轴线(横) + p->drawLine(axisLeft, axisBottom, W - legendMarginR, axisBottom); + + // 刻度数(6~8 个) + const int nTick = 7; + QFont tickFont = p->font(); + tickFont.setPixelSize(10); + p->setFont(tickFont); + + // X 轴刻度(下轴) + const double xRange = sceneXMax - sceneXMin; + if (xRange > 0) { + for (int t = 0; t <= nTick; ++t) { + const double sx = sceneXMin + xRange * t / nTick; + const QPoint vp = mapFromScene(QPointF(sx, sceneYMin)); // 场景 → 视口像素 + const int px = vp.x(); + if (px < axisLeft || px > W - legendMarginR) continue; + p->setPen(QPen(QColor(100, 100, 100), 1)); + p->drawLine(px, axisBottom, px, axisBottom + 4); + p->setPen(Qt::black); + const QString label = QString::number(sx, 'g', 4); + p->drawText(QRect(px - 20, axisBottom + 5, 40, 12), Qt::AlignCenter, label); + } + } + + // Y 轴刻度(左轴) + const double yRange = sceneYMax - sceneYMin; + if (yRange > 0) { + for (int t = 0; t <= nTick; ++t) { + const double sy = sceneYMin + yRange * t / nTick; + const QPoint vp = mapFromScene(QPointF(sceneXMin, sy)); + const int py = vp.y(); + if (py < 4 || py > axisBottom) continue; + p->setPen(QPen(QColor(100, 100, 100), 1)); + p->drawLine(axisLeft - 4, py, axisLeft, py); + p->setPen(Qt::black); + const QString label = QString::number(sy, 'g', 4); + p->drawText(QRect(0, py - 7, axisLeft - 5, 14), Qt::AlignRight | Qt::AlignVCenter, label); + } + } + + p->restore(); +} + +} // namespace geopro::app diff --git a/src/app/panels/chart/DatasetChartView.hpp b/src/app/panels/chart/DatasetChartView.hpp new file mode 100644 index 0000000..8533181 --- /dev/null +++ b/src/app/panels/chart/DatasetChartView.hpp @@ -0,0 +1,38 @@ +#pragma once +#include +#include +#include +#include "model/Field.hpp" +#include "model/ColorScale.hpp" +#include "model/Anomaly.hpp" +#include "ContourBands.hpp" +namespace geopro::app { + +// 平面图表视图:散点(原数据) / 等值面(网格) + 异常叠加 + 色阶图例。VTK 仅经 ContourBands 算几何。 +class DatasetChartView : public QGraphicsView { + Q_OBJECT +public: + explicit DatasetChartView(QWidget* parent = nullptr); + void showScatter(const geopro::core::ScatterField& f, const geopro::core::ColorScale& cs); + void showContour(const geopro::core::Grid& g, const geopro::core::ColorScale& cs, + const geopro::render::ContourOptions& opt); + void setAnomalies(const std::vector& list); + void setHiddenAnomalies(const std::set& hidden); // 下标=list 序 + void setShowAnomalies(bool on); + void setShowContourLines(bool on); + void clearChart(); +protected: + void wheelEvent(QWheelEvent* e) override; // 滚轮缩放 + void drawForeground(QPainter* p, const QRectF& rect) override; +private: + void rebuildAnomalyItems(); + void setLegend(const geopro::core::ColorScale& cs) { legendScale_ = cs; } + QGraphicsScene* scene_; + std::vector anomalies_; + std::set hidden_; + bool showAnomalies_ = true; + bool showContourLines_ = true; + std::vector anomalyItems_; + geopro::core::ColorScale legendScale_; +}; +} // namespace geopro::app