feat/dataset-detail-chart #5

Merged
gaozheng merged 74 commits from feat/dataset-detail-chart into main 2026-06-13 17:30:37 +08:00
3 changed files with 255 additions and 0 deletions
Showing only changes of commit facb812bca - Show all commits

View File

@ -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)

View File

@ -0,0 +1,216 @@
#include "panels/chart/DatasetChartView.hpp"
#include <QGraphicsScene>
#include <QGraphicsPathItem>
#include <QGraphicsRectItem>
#include <QPainterPath>
#include <QPen>
#include <QBrush>
#include <QColor>
#include <QWheelEvent>
#include <QPainter>
#include <QFontMetrics>
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<geopro::core::Anomaly>& list) {
anomalies_ = list; rebuildAnomalyItems();
}
void DatasetChartView::setHiddenAnomalies(const std::set<int>& 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<int>(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<int>(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<double> stops = legendScale_.stopValues();
if (stops.size() >= 2 && legendW > 0) {
const int nSeg = static_cast<int>(stops.size()) - 1;
const double segW = static_cast<double>(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<int>(s * segW);
const int x1 = legendMarginL + static_cast<int>((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<int>(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

View File

@ -0,0 +1,38 @@
#pragma once
#include <set>
#include <vector>
#include <QGraphicsView>
#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<geopro::core::Anomaly>& list);
void setHiddenAnomalies(const std::set<int>& 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<geopro::core::Anomaly> anomalies_;
std::set<int> hidden_;
bool showAnomalies_ = true;
bool showContourLines_ = true;
std::vector<class QGraphicsItem*> anomalyItems_;
geopro::core::ColorScale legendScale_;
};
} // namespace geopro::app