feat/dataset-detail-chart #5
|
|
@ -29,6 +29,9 @@ add_executable(geopro_desktop WIN32
|
|||
panels/DescriptionPanel.cpp
|
||||
panels/chart/RawDataChartView.cpp
|
||||
panels/chart/GridDataChartView.cpp
|
||||
panels/chart/ColorMapService.cpp
|
||||
panels/chart/ColorBarWidget.cpp
|
||||
panels/chart/ScatterPlotItem.cpp
|
||||
panels/AnomalyTablePanel.cpp
|
||||
panels/DatasetDetailPage.cpp
|
||||
panels/DatasetDetailPanel.cpp
|
||||
|
|
@ -53,7 +56,9 @@ target_link_libraries(geopro_desktop PRIVATE
|
|||
geopro_controller # Phase 5:导航编排(WorkbenchNavController)
|
||||
)
|
||||
|
||||
# Qwt(二维科学图表;源码存在时由根 CMake 定义 qwt 目标)。本步骤骨架先链好供后续渲染步骤使用。
|
||||
# Qwt(二维科学图表;源码存在时由根 CMake 定义 qwt 目标)。
|
||||
# cmake/qwt.cmake 已加 QWT_MOC_INCLUDE=1,Qwt Q_OBJECT 类的 MOC 代码直接编译进各 .cpp.obj,
|
||||
# 标准按需链接可正确解析所有 MOC 符号(无需 /WHOLEARCHIVE)。
|
||||
if(TARGET qwt)
|
||||
target_link_libraries(geopro_desktop PRIVATE qwt)
|
||||
endif()
|
||||
|
|
|
|||
|
|
@ -0,0 +1,87 @@
|
|||
#include "panels/chart/ColorBarWidget.hpp"
|
||||
#include <QPainter>
|
||||
#include <QPaintEvent>
|
||||
|
||||
namespace geopro::app {
|
||||
|
||||
static constexpr int kBarHeight = 18; // 色带高度(px)
|
||||
static constexpr int kTickHeight = 4; // 刻度短线高度(px)
|
||||
static constexpr int kFontSize = 9; // 刻度字号
|
||||
|
||||
ColorBarWidget::ColorBarWidget(QWidget* parent)
|
||||
: QWidget(parent) {
|
||||
setFixedHeight(36);
|
||||
}
|
||||
|
||||
void ColorBarWidget::setColorScale(const core::ColorScale& scale) {
|
||||
scale_ = scale;
|
||||
update();
|
||||
}
|
||||
|
||||
void ColorBarWidget::paintEvent(QPaintEvent* /*event*/) {
|
||||
auto stops = scale_.stops();
|
||||
if (stops.empty()) return;
|
||||
|
||||
QPainter p(this);
|
||||
p.setRenderHint(QPainter::Antialiasing, false);
|
||||
|
||||
const int W = width();
|
||||
const int H = height();
|
||||
|
||||
// 横向色带区域(顶部)
|
||||
const int barY = 2;
|
||||
const int barH = kBarHeight;
|
||||
|
||||
// 边界值
|
||||
double minVal = stops.front().first;
|
||||
double maxVal = stops.back().first;
|
||||
double range = maxVal - minVal;
|
||||
if (range <= 0.0) range = 1.0;
|
||||
|
||||
// 绘制每段色格(相邻断点间一格)
|
||||
for (std::size_t i = 0; i + 1 < stops.size(); ++i) {
|
||||
double posL = (stops[i].first - minVal) / range;
|
||||
double posR = (stops[i + 1].first - minVal) / range;
|
||||
int xL = static_cast<int>(posL * (W - 1));
|
||||
int xR = static_cast<int>(posR * (W - 1));
|
||||
if (xR <= xL) xR = xL + 1;
|
||||
const auto& c = stops[i].second;
|
||||
p.fillRect(xL, barY, xR - xL, barH, QColor(c.r, c.g, c.b, c.a));
|
||||
}
|
||||
// 最后一段(末断点颜色填到边缘)
|
||||
{
|
||||
double posL = (stops.back().first - minVal) / range;
|
||||
int xL = static_cast<int>(posL * (W - 1));
|
||||
const auto& c = stops.back().second;
|
||||
p.fillRect(xL, barY, W - xL, barH, QColor(c.r, c.g, c.b, c.a));
|
||||
}
|
||||
|
||||
// 外边框
|
||||
p.setPen(QPen(QColor(80, 80, 80), 1));
|
||||
p.drawRect(0, barY, W - 1, barH - 1);
|
||||
|
||||
// 刻度区(色带下方)
|
||||
QFont font = p.font();
|
||||
font.setPixelSize(kFontSize);
|
||||
p.setFont(font);
|
||||
p.setPen(QColor(200, 200, 200));
|
||||
|
||||
const int tickY = barY + barH + 1;
|
||||
|
||||
for (const auto& [val, color] : stops) {
|
||||
double pos = (val - minVal) / range;
|
||||
int x = static_cast<int>(pos * (W - 1));
|
||||
// 刻度短线
|
||||
p.drawLine(x, tickY, x, tickY + kTickHeight);
|
||||
// 数值文字(右对齐到刻度位置,避免越界)
|
||||
QString label = QString::number(val, 'g', 4);
|
||||
QFontMetrics fm(font);
|
||||
int tw = fm.horizontalAdvance(label);
|
||||
int tx = x - tw / 2;
|
||||
if (tx < 0) tx = 0;
|
||||
if (tx + tw > W) tx = W - tw;
|
||||
p.drawText(tx, H - 1, label);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace geopro::app
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
#pragma once
|
||||
#include "model/ColorScale.hpp"
|
||||
#include <QWidget>
|
||||
|
||||
namespace geopro::app {
|
||||
|
||||
// 独立色阶条 Widget,水平排布:上方彩色色带 + 下方刻度值。
|
||||
// 作为 QwtPlot 的兄弟 widget 布局在图表下方,不进入 Qwt 坐标系,
|
||||
// 因此不随图表缩放/平移移动,也不与轴标注重叠。
|
||||
class ColorBarWidget : public QWidget {
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit ColorBarWidget(QWidget* parent = nullptr);
|
||||
|
||||
void setColorScale(const core::ColorScale& scale);
|
||||
|
||||
protected:
|
||||
void paintEvent(QPaintEvent* event) override;
|
||||
|
||||
private:
|
||||
core::ColorScale scale_;
|
||||
};
|
||||
|
||||
} // namespace geopro::app
|
||||
|
|
@ -1,4 +1,6 @@
|
|||
#include "panels/chart/RawDataChartView.hpp"
|
||||
#include "panels/chart/ColorBarWidget.hpp"
|
||||
#include "panels/chart/ScatterPlotItem.hpp"
|
||||
|
||||
#include <QComboBox>
|
||||
#include <QHBoxLayout>
|
||||
|
|
@ -6,6 +8,11 @@
|
|||
#include <QToolButton>
|
||||
#include <QVBoxLayout>
|
||||
|
||||
#include <qwt_plot.h>
|
||||
#include <qwt_plot_canvas.h>
|
||||
#include <qwt_plot_panner.h>
|
||||
#include <qwt_plot_magnifier.h>
|
||||
|
||||
namespace geopro::app {
|
||||
|
||||
RawDataChartView::RawDataChartView(QWidget* parent) : QWidget(parent) {
|
||||
|
|
@ -42,21 +49,70 @@ RawDataChartView::RawDataChartView(QWidget* parent) : QWidget(parent) {
|
|||
|
||||
lay->addWidget(toolbar);
|
||||
|
||||
// ---- 图表占位区(stretch) ----
|
||||
plotArea_ = new QWidget(this);
|
||||
plotArea_->setObjectName(QStringLiteral("rawPlotArea"));
|
||||
lay->addWidget(plotArea_, 1);
|
||||
// ---- QwtPlot(stretch 填满剩余空间)----
|
||||
plot_ = new QwtPlot(this);
|
||||
plot_->setObjectName(QStringLiteral("rawPlotArea"));
|
||||
|
||||
// ---- 色阶占位(固定高 36px) ----
|
||||
auto* colorScaleBar = new QWidget(this);
|
||||
colorScaleBar->setObjectName(QStringLiteral("rawColorScaleBar"));
|
||||
colorScaleBar->setFixedHeight(36);
|
||||
lay->addWidget(colorScaleBar);
|
||||
// x 轴顶部,关闭底部 x 轴
|
||||
plot_->enableAxis(QwtPlot::xTop, true);
|
||||
plot_->enableAxis(QwtPlot::xBottom, false);
|
||||
plot_->enableAxis(QwtPlot::yLeft, true);
|
||||
|
||||
// 深色背景风格
|
||||
plot_->setCanvasBackground(QBrush(QColor(30, 30, 30)));
|
||||
|
||||
// 交互:Panner(左键拖动平移)+ Magnifier(滚轮缩放)
|
||||
auto* panner = new QwtPlotPanner(plot_->canvas());
|
||||
panner->setMouseButton(Qt::LeftButton);
|
||||
|
||||
new QwtPlotMagnifier(plot_->canvas());
|
||||
|
||||
lay->addWidget(plot_, 1);
|
||||
|
||||
// ---- 独立色阶条(固定高 36px,QwtPlot 的兄弟 widget)----
|
||||
colorBar_ = new ColorBarWidget(this);
|
||||
colorBar_->setObjectName(QStringLiteral("rawColorScaleBar"));
|
||||
lay->addWidget(colorBar_);
|
||||
}
|
||||
|
||||
void RawDataChartView::setData(const geopro::controller::DatasetDetailController::ChartData& d) {
|
||||
// 步骤1:保存数据,图表渲染留给后续步骤。
|
||||
QWidget* RawDataChartView::plotArea() const {
|
||||
return plot_;
|
||||
}
|
||||
|
||||
void RawDataChartView::setData(
|
||||
const geopro::controller::DatasetDetailController::ChartData& d) {
|
||||
data_ = d;
|
||||
|
||||
if (d.scatterScale.empty()) return;
|
||||
|
||||
// 重建 ColorMapService(旧的 scatterItem 已被 QwtPlot detach/delete)
|
||||
delete colorSvc_;
|
||||
colorSvc_ = new ColorMapService(d.scatterScale);
|
||||
|
||||
// 卸载旧散点项
|
||||
if (scatterItem_) {
|
||||
scatterItem_->detach();
|
||||
// QwtPlot 不拥有 item,需要我们释放
|
||||
delete scatterItem_;
|
||||
scatterItem_ = nullptr;
|
||||
}
|
||||
|
||||
// 新建散点项并挂到 plot
|
||||
scatterItem_ = new ScatterPlotItem();
|
||||
scatterItem_->setData(d.scatter, colorSvc_);
|
||||
scatterItem_->attach(plot_);
|
||||
|
||||
// 按数据包围盒设置轴范围
|
||||
QRectF bbox = scatterItem_->boundingRect();
|
||||
if (!bbox.isEmpty()) {
|
||||
plot_->setAxisScale(QwtPlot::xTop, bbox.left(), bbox.right());
|
||||
plot_->setAxisScale(QwtPlot::yLeft, bbox.top(), bbox.bottom());
|
||||
}
|
||||
|
||||
plot_->replot();
|
||||
|
||||
// 更新色阶条
|
||||
colorBar_->setColorScale(d.scatterScale);
|
||||
}
|
||||
|
||||
} // namespace geopro::app
|
||||
|
|
|
|||
|
|
@ -1,28 +1,36 @@
|
|||
#pragma once
|
||||
#include <QWidget>
|
||||
#include "DatasetDetailController.hpp"
|
||||
#include "panels/chart/ColorMapService.hpp"
|
||||
|
||||
class QComboBox;
|
||||
class QwtPlot;
|
||||
|
||||
namespace geopro::app {
|
||||
|
||||
// 原数据图表视图(步骤1骨架):工具条 + 图表占位 + 色阶占位。
|
||||
// 图表渲染由后续步骤填入 plotArea()。
|
||||
class ColorBarWidget;
|
||||
class ScatterPlotItem;
|
||||
|
||||
// 原数据图表视图:工具条 + QwtPlot(x 轴顶部、Panner/Magnifier)+ 独立色阶条。
|
||||
class RawDataChartView : public QWidget {
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit RawDataChartView(QWidget* parent = nullptr);
|
||||
|
||||
// 步骤1:保存数据备后续渲染步骤使用;图表区留空。
|
||||
void setData(const geopro::controller::DatasetDetailController::ChartData& d);
|
||||
|
||||
// 供后续步骤填充 Qwt 画布的占位区域。
|
||||
QWidget* plotArea() const { return plotArea_; }
|
||||
// 供外部访问(已不再是占位,保留兼容接口返回 plot_)
|
||||
QWidget* plotArea() const;
|
||||
|
||||
private:
|
||||
geopro::controller::DatasetDetailController::ChartData data_;
|
||||
QWidget* plotArea_;
|
||||
QComboBox* chartTypeCombo_;
|
||||
QwtPlot* plot_;
|
||||
ColorBarWidget* colorBar_;
|
||||
QComboBox* chartTypeCombo_;
|
||||
|
||||
// 使用 unique_ptr 管理生命周期;attach 后 QwtPlot 接管绘制,但我们仍持有指针
|
||||
ColorMapService* colorSvc_ = nullptr; // heap,由 setData 重建
|
||||
ScatterPlotItem* scatterItem_ = nullptr;
|
||||
};
|
||||
|
||||
} // namespace geopro::app
|
||||
|
|
|
|||
|
|
@ -0,0 +1,67 @@
|
|||
#include "panels/chart/ScatterPlotItem.hpp"
|
||||
#include <QPainter>
|
||||
#include <qwt_scale_map.h>
|
||||
#include <algorithm>
|
||||
#include <limits>
|
||||
|
||||
namespace geopro::app {
|
||||
|
||||
ScatterPlotItem::ScatterPlotItem()
|
||||
: QwtPlotItem() {
|
||||
setTitle("Scatter");
|
||||
setRenderHint(QwtPlotItem::RenderAntialiased, false);
|
||||
}
|
||||
|
||||
void ScatterPlotItem::setData(const core::ScatterField& field, ColorMapService* svc) {
|
||||
field_ = field;
|
||||
colorSvc_ = svc;
|
||||
|
||||
// 计算数据包围盒
|
||||
if (field_.x.empty() || field_.y.empty()) {
|
||||
bounding_ = QRectF();
|
||||
return;
|
||||
}
|
||||
double xMin = *std::min_element(field_.x.begin(), field_.x.end());
|
||||
double xMax = *std::max_element(field_.x.begin(), field_.x.end());
|
||||
double yMin = *std::min_element(field_.y.begin(), field_.y.end());
|
||||
double yMax = *std::max_element(field_.y.begin(), field_.y.end());
|
||||
// 加少量 margin,避免边界点被截
|
||||
double xM = (xMax - xMin) * 0.03 + 0.1;
|
||||
double yM = (yMax - yMin) * 0.03 + 0.1;
|
||||
bounding_ = QRectF(xMin - xM, yMin - yM,
|
||||
(xMax - xMin) + 2.0 * xM,
|
||||
(yMax - yMin) + 2.0 * yM);
|
||||
}
|
||||
|
||||
QRectF ScatterPlotItem::boundingRect() const {
|
||||
return bounding_;
|
||||
}
|
||||
|
||||
void ScatterPlotItem::draw(QPainter* painter,
|
||||
const QwtScaleMap& xMap,
|
||||
const QwtScaleMap& yMap,
|
||||
const QRectF& /*canvasRect*/) const {
|
||||
if (!colorSvc_) return;
|
||||
const auto& xs = field_.x;
|
||||
const auto& ys = field_.y;
|
||||
const auto& vs = field_.v;
|
||||
const std::size_t n = xs.size();
|
||||
if (n == 0) return;
|
||||
|
||||
painter->save();
|
||||
painter->setRenderHint(QPainter::Antialiasing, false);
|
||||
painter->setPen(QPen(Qt::white, kPenWidth));
|
||||
|
||||
for (std::size_t i = 0; i < n; ++i) {
|
||||
double px = xMap.transform(xs[i]);
|
||||
double py = yMap.transform(ys[i]);
|
||||
double val = (i < vs.size()) ? vs[i] : 0.0;
|
||||
auto c = colorSvc_->colorAtContinuous(val);
|
||||
painter->setBrush(QColor(c.r, c.g, c.b, c.a));
|
||||
painter->drawRect(QRectF(px - kHalfSide, py - kHalfSide,
|
||||
kHalfSide * 2.0, kHalfSide * 2.0));
|
||||
}
|
||||
painter->restore();
|
||||
}
|
||||
|
||||
} // namespace geopro::app
|
||||
|
|
@ -0,0 +1,39 @@
|
|||
#pragma once
|
||||
#include "model/Field.hpp"
|
||||
#include "panels/chart/ColorMapService.hpp"
|
||||
#include <qwt_plot_item.h>
|
||||
#include <QRectF>
|
||||
|
||||
class QPainter;
|
||||
class QwtScaleMap;
|
||||
|
||||
namespace geopro::app {
|
||||
|
||||
// QwtPlotItem:把 ScatterField 数据渲染为彩色方块散点。
|
||||
// 每个点用固定像素边长方块绘制(不随缩放变大),白色描边,
|
||||
// 颜色由 ColorMapService 连续插值决定(与原版 Plotly 一致)。
|
||||
class ScatterPlotItem : public QwtPlotItem {
|
||||
public:
|
||||
ScatterPlotItem();
|
||||
|
||||
void setData(const core::ScatterField& field, ColorMapService* svc);
|
||||
|
||||
int rtti() const override { return QwtPlotItem::Rtti_PlotUserItem; }
|
||||
|
||||
QRectF boundingRect() const override;
|
||||
|
||||
void draw(QPainter* painter,
|
||||
const QwtScaleMap& xMap,
|
||||
const QwtScaleMap& yMap,
|
||||
const QRectF& canvasRect) const override;
|
||||
|
||||
private:
|
||||
core::ScatterField field_;
|
||||
ColorMapService* colorSvc_ = nullptr; // 不拥有,由 RawDataChartView 持有
|
||||
QRectF bounding_;
|
||||
|
||||
static constexpr double kHalfSide = 3.5; // 方块半边长(像素)
|
||||
static constexpr double kPenWidth = 1.0; // 白色描边宽度(像素)
|
||||
};
|
||||
|
||||
} // namespace geopro::app
|
||||
Loading…
Reference in New Issue