fix(detail): measurement 对话框/工具条视觉返工对齐原版
以原版 web 为准返工 measurement 散点交互视觉: - 数据过滤:1000px;左直方图(hover柱变红+tooltip)+右信息区(数值范围/占比/原始点数/ 当前点数橙色高亮)+底部双手柄范围滑块(新增RangeSlider)+计算分布/重置;min/max输入 最大在上最小在下;三方联动(输入↔滑块↔直方图) - 另存为(RawData):280px、标题"数据另存为"、确认/取消 - 色阶/另存/过滤成功 toast - 信息面板 A红/B蓝/M绿/N橙(#F4B008);tooltip"查看散点属性"/"散点的点选" - X/Y/V/值类型下拉固定宽 120/160/160/120;无高程禁用 X/Y - 导出置工具条最右(页头HeaderAction跨ddCode静态) API 字段未动。build all 绿。
This commit is contained in:
parent
687edfeca1
commit
c21226a3d7
|
|
@ -46,6 +46,7 @@ add_executable(geopro_desktop WIN32
|
|||
panels/chart/SaveAsDialog.cpp
|
||||
panels/chart/ScatterFilterDialog.cpp
|
||||
panels/chart/ScatterHistogram.cpp
|
||||
panels/chart/RangeSlider.cpp
|
||||
panels/chart/InversionProcessOps.cpp
|
||||
panels/chart/GridWizardDialog.cpp
|
||||
panels/chart/WhiteningDialog.cpp
|
||||
|
|
|
|||
|
|
@ -0,0 +1,104 @@
|
|||
#include "panels/chart/RangeSlider.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
|
||||
#include <QMouseEvent>
|
||||
#include <QPainter>
|
||||
|
||||
namespace geopro::app {
|
||||
|
||||
namespace {
|
||||
constexpr int kHandleR = 7; // 手柄半径(像素)
|
||||
constexpr int kTrackH = 4; // 轨道高度
|
||||
constexpr int kMargin = kHandleR + 2; // 左右留边,保证端点手柄不被裁
|
||||
const QColor kTrackBg(229, 230, 235); // 轨道底(浅灰)
|
||||
const QColor kTrackSel(245, 63, 63); // 选中段:红(对照原版滑轨 #f53f3f)
|
||||
const QColor kHandleFill(255, 255, 255);
|
||||
const QColor kHandleBorder(245, 63, 63); // 手柄边框红(对照原版)
|
||||
} // namespace
|
||||
|
||||
RangeSlider::RangeSlider(QWidget* parent) : QWidget(parent) {
|
||||
setMinimumHeight(2 * kHandleR + 6);
|
||||
setMouseTracking(false);
|
||||
setCursor(Qt::PointingHandCursor);
|
||||
}
|
||||
|
||||
void RangeSlider::setRange(double min, double max) {
|
||||
min_ = min;
|
||||
max_ = (max > min) ? max : min + 1.0; // 退化区间兜底,避免除零
|
||||
low_ = std::clamp(low_, min_, max_);
|
||||
high_ = std::clamp(high_, min_, max_);
|
||||
update();
|
||||
}
|
||||
|
||||
void RangeSlider::setValues(double low, double high) {
|
||||
low_ = std::clamp(std::min(low, high), min_, max_);
|
||||
high_ = std::clamp(std::max(low, high), min_, max_);
|
||||
update();
|
||||
}
|
||||
|
||||
int RangeSlider::valueToX(double v) const {
|
||||
const int usable = width() - 2 * kMargin;
|
||||
if (usable <= 0 || max_ <= min_) return kMargin;
|
||||
return kMargin + static_cast<int>((v - min_) / (max_ - min_) * usable);
|
||||
}
|
||||
|
||||
double RangeSlider::xToValue(int px) const {
|
||||
const int usable = width() - 2 * kMargin;
|
||||
if (usable <= 0) return min_;
|
||||
const double t = std::clamp(static_cast<double>(px - kMargin) / usable, 0.0, 1.0);
|
||||
return min_ + t * (max_ - min_);
|
||||
}
|
||||
|
||||
void RangeSlider::paintEvent(QPaintEvent*) {
|
||||
QPainter p(this);
|
||||
p.setRenderHint(QPainter::Antialiasing, true);
|
||||
const int cy = height() / 2;
|
||||
const int xLo = valueToX(low_);
|
||||
const int xHi = valueToX(high_);
|
||||
|
||||
// 轨道底
|
||||
p.setPen(Qt::NoPen);
|
||||
p.setBrush(kTrackBg);
|
||||
p.drawRoundedRect(QRect(kMargin, cy - kTrackH / 2, width() - 2 * kMargin, kTrackH), 2, 2);
|
||||
// 选中段(红)
|
||||
p.setBrush(kTrackSel);
|
||||
p.drawRect(QRect(xLo, cy - kTrackH / 2, std::max(0, xHi - xLo), kTrackH));
|
||||
|
||||
// 两个手柄
|
||||
QPen border(kHandleBorder, 2);
|
||||
p.setPen(border);
|
||||
p.setBrush(kHandleFill);
|
||||
p.drawEllipse(QPoint(xLo, cy), kHandleR, kHandleR);
|
||||
p.drawEllipse(QPoint(xHi, cy), kHandleR, kHandleR);
|
||||
}
|
||||
|
||||
void RangeSlider::mousePressEvent(QMouseEvent* event) {
|
||||
const int px = event->position().toPoint().x();
|
||||
const int dLo = std::abs(px - valueToX(low_));
|
||||
const int dHi = std::abs(px - valueToX(high_));
|
||||
// 就近选手柄;距离相等时按点击位置相对中点决定(左半选低,右半选高)。
|
||||
if (dLo == dHi)
|
||||
dragging_ = (px < valueToX((low_ + high_) / 2.0)) ? 1 : 2;
|
||||
else
|
||||
dragging_ = (dLo < dHi) ? 1 : 2;
|
||||
mouseMoveEvent(event);
|
||||
}
|
||||
|
||||
void RangeSlider::mouseMoveEvent(QMouseEvent* event) {
|
||||
if (dragging_ == 0) return;
|
||||
const double v = xToValue(event->position().toPoint().x());
|
||||
if (dragging_ == 1)
|
||||
low_ = std::min(v, high_);
|
||||
else
|
||||
high_ = std::max(v, low_);
|
||||
update();
|
||||
emit rangeChanged(low_, high_);
|
||||
}
|
||||
|
||||
void RangeSlider::mouseReleaseEvent(QMouseEvent*) {
|
||||
dragging_ = 0;
|
||||
}
|
||||
|
||||
} // namespace geopro::app
|
||||
|
|
@ -0,0 +1,40 @@
|
|||
#pragma once
|
||||
#include <QWidget>
|
||||
|
||||
namespace geopro::app {
|
||||
|
||||
// 双手柄范围滑块(M3 数据过滤底部 a-slider range 的 Qt 复刻)。
|
||||
// Qt 无原生双手柄滑块,故自绘:一条水平轨道 + 两个圆形手柄(低/高),
|
||||
// 选中段轨道用强调红(对照原版滑轨 #f53f3f)。值域用 double(连续,对照原版 step 0.01)。
|
||||
// 拖动手柄发 rangeChanged(low, high);外部 setRange/setValues 同步。
|
||||
class RangeSlider : public QWidget {
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit RangeSlider(QWidget* parent = nullptr);
|
||||
|
||||
void setRange(double min, double max); // 设定值域(数据 min/max)
|
||||
void setValues(double low, double high); // 设定当前低/高手柄值(外部联动用,不发信号)
|
||||
double lowValue() const { return low_; }
|
||||
double highValue() const { return high_; }
|
||||
|
||||
signals:
|
||||
void rangeChanged(double low, double high);
|
||||
|
||||
protected:
|
||||
void paintEvent(QPaintEvent* event) override;
|
||||
void mousePressEvent(QMouseEvent* event) override;
|
||||
void mouseMoveEvent(QMouseEvent* event) override;
|
||||
void mouseReleaseEvent(QMouseEvent* event) override;
|
||||
|
||||
private:
|
||||
int valueToX(double v) const; // 值 → 像素 x
|
||||
double xToValue(int px) const; // 像素 x → 值(夹到值域)
|
||||
|
||||
double min_ = 0.0;
|
||||
double max_ = 1.0;
|
||||
double low_ = 0.0;
|
||||
double high_ = 1.0;
|
||||
int dragging_ = 0; // 0=无,1=低手柄,2=高手柄
|
||||
};
|
||||
|
||||
} // namespace geopro::app
|
||||
|
|
@ -53,6 +53,7 @@
|
|||
|
||||
#include "panels/chart/LivePanner.hpp"
|
||||
#include "Theme.hpp"
|
||||
#include "ToastOverlay.hpp" // showToast:统一成功轻提示(规范 §7.7)
|
||||
|
||||
namespace geopro::app {
|
||||
|
||||
|
|
@ -213,6 +214,12 @@ void fillCombo(QComboBox* combo, const std::vector<geopro::core::FieldOption>& o
|
|||
constexpr int kToolIconPx = 16; // 逻辑图标边长(与 setIconSize 对齐)
|
||||
constexpr qreal kToolIconScale = 2.0; // 超采样倍率(HiDPI 清晰)
|
||||
|
||||
// measurement 工具条下拉固定宽度(对照原版 datasetTool.vue 各 a-select 的 width)。
|
||||
constexpr int kComboW_X = 120; // X 下拉 width:120px
|
||||
constexpr int kComboW_Y = 160; // Y 下拉 width:160px
|
||||
constexpr int kComboW_V = 160; // V 值下拉 width:160px
|
||||
constexpr int kComboW_ValueType = 120; // 值类型下拉 width:120px
|
||||
|
||||
QPixmap makeToolIconCanvas(QPainter& p) {
|
||||
// 调用方在 [0,kToolIconPx] 逻辑坐标系下作画;返回前缩放 + 设 dpr。
|
||||
const int dim = qRound(kToolIconPx * kToolIconScale);
|
||||
|
|
@ -390,6 +397,7 @@ void RawDataChartView::openInversionColorScale(QWidget* anchor) {
|
|||
colorSvc_ = new ColorMapService(data_.scale);
|
||||
redrawScatter();
|
||||
colorBar_->setColorScale(data_.scale);
|
||||
showToast(this, QStringLiteral("色阶应用成功")); // M8 成功提示(对照原版 Message.success)
|
||||
|
||||
// 持久化(businessCode 空,对照原版 originPage newLvlColorLevel businessCode:'')。
|
||||
const QString dsId = dsIdGetter_ ? dsIdGetter_() : QString();
|
||||
|
|
@ -568,6 +576,7 @@ void RawDataChartView::openScatterColorScale(QWidget* anchor) {
|
|||
// 同步右侧竖条/底部横条色阶图例。
|
||||
if (data_.verticalLegend) colorBarV_->setColorScale(data_.scale);
|
||||
else colorBar_->setColorScale(data_.scale);
|
||||
showToast(this, QStringLiteral("色阶应用成功")); // M8 成功提示(对照原版 Message.success)
|
||||
|
||||
// 持久化到后端(saveColorGradation,businessCode=当前 V 值,type=3 散点路径)。
|
||||
const QString dsId = dsIdGetter_ ? dsIdGetter_() : QString();
|
||||
|
|
@ -624,17 +633,38 @@ void RawDataChartView::toggleInfoMode(bool on) {
|
|||
infoMode_ = on;
|
||||
if (on && !infoPanel_) {
|
||||
// 首次开启:建覆盖在图区右上角的属性面板(复刻原版 .scatterInfos 浮层)。
|
||||
// A/B/M/N 按原版逐项配色(item-label-a 红 / -b 蓝 / -m 绿 / -n 橙#F4B008)。
|
||||
infoPanel_ = new QWidget(plot_->canvas());
|
||||
infoPanel_->setObjectName(QStringLiteral("scatterInfoPanel"));
|
||||
auto* il = new QVBoxLayout(infoPanel_);
|
||||
il->setContentsMargins(8, 6, 8, 6);
|
||||
infoLabel_ = new QLabel(QStringLiteral("点选散点查看属性"), infoPanel_);
|
||||
il->addWidget(infoLabel_);
|
||||
il->setSpacing(2);
|
||||
infoHint_ = new QLabel(QStringLiteral("点选散点查看属性"), infoPanel_);
|
||||
il->addWidget(infoHint_);
|
||||
// 各属性行:标签上色(label QSS 不染行值),初始隐藏,命中点后填值显示。
|
||||
infoValA_ = new QLabel(infoPanel_);
|
||||
infoValA_->setObjectName(QStringLiteral("infoA"));
|
||||
infoValB_ = new QLabel(infoPanel_);
|
||||
infoValB_->setObjectName(QStringLiteral("infoB"));
|
||||
infoValM_ = new QLabel(infoPanel_);
|
||||
infoValM_->setObjectName(QStringLiteral("infoM"));
|
||||
infoValN_ = new QLabel(infoPanel_);
|
||||
infoValN_->setObjectName(QStringLiteral("infoN"));
|
||||
infoValRow_ = new QLabel(infoPanel_);
|
||||
infoValPseu_ = new QLabel(infoPanel_);
|
||||
for (QLabel* l : {infoValA_, infoValB_, infoValM_, infoValN_, infoValRow_, infoValPseu_}) {
|
||||
l->setVisible(false);
|
||||
il->addWidget(l);
|
||||
}
|
||||
applyTokenizedStyleSheet(
|
||||
infoPanel_,
|
||||
QStringLiteral("QWidget#scatterInfoPanel { background: {{bg/panel}};"
|
||||
" border: 1px solid {{border/default}}; border-radius: 6px; }"
|
||||
"QLabel { color: {{text/primary}}; }"));
|
||||
"QLabel { color: {{text/primary}}; }"
|
||||
"QLabel#infoA { color: #FF0000; }" // A 红
|
||||
"QLabel#infoB { color: #0000FF; }" // B 蓝
|
||||
"QLabel#infoM { color: #008000; }" // M 绿
|
||||
"QLabel#infoN { color: #F4B008; }")); // N 橙黄
|
||||
// 画布事件过滤器:信息模式下点击找最近点显示属性。
|
||||
plot_->canvas()->installEventFilter(this);
|
||||
}
|
||||
|
|
@ -649,7 +679,7 @@ void RawDataChartView::toggleInfoMode(bool on) {
|
|||
}
|
||||
|
||||
void RawDataChartView::showPointInfoAt(const QPoint& canvasPos) {
|
||||
if (!infoLabel_ || data_.scatter.x.empty()) return;
|
||||
if (!infoValA_ || data_.scatter.x.empty()) return;
|
||||
const QwtScaleMap xMap = plot_->canvasMap(QwtPlot::xTop);
|
||||
const QwtScaleMap yMap = plot_->canvasMap(QwtPlot::yLeft);
|
||||
const auto& s = data_.scatter;
|
||||
|
|
@ -667,14 +697,17 @@ void RawDataChartView::showPointInfoAt(const QPoint& canvasPos) {
|
|||
auto at = [](const std::vector<double>& v, std::size_t i) {
|
||||
return i < v.size() ? v[i] : 0.0;
|
||||
};
|
||||
// 复刻原版 scatterInfos:A / B / M / N / DataRow / Pseu_Resis。
|
||||
infoLabel_->setText(QStringLiteral("A= %1\nB= %2\nM= %3\nN= %4\nDataRow= %5\nPseu_Resis= %6")
|
||||
.arg(QString::number(at(s.a, bestI), 'g', 6),
|
||||
QString::number(at(s.b, bestI), 'g', 6),
|
||||
QString::number(at(s.m, bestI), 'g', 6),
|
||||
QString::number(at(s.n, bestI), 'g', 6),
|
||||
QString::number(at(s.row, bestI), 'g', 6),
|
||||
QString::number(at(s.pseu, bestI), 'g', 6)));
|
||||
// 复刻原版 scatterInfos:A / B / M / N / DataRow / Pseu_Resis(A/B/M/N 标签逐项配色)。
|
||||
if (infoHint_) infoHint_->setVisible(false);
|
||||
infoValA_->setText(QStringLiteral("A= %1").arg(QString::number(at(s.a, bestI), 'g', 6)));
|
||||
infoValB_->setText(QStringLiteral("B= %1").arg(QString::number(at(s.b, bestI), 'g', 6)));
|
||||
infoValM_->setText(QStringLiteral("M= %1").arg(QString::number(at(s.m, bestI), 'g', 6)));
|
||||
infoValN_->setText(QStringLiteral("N= %1").arg(QString::number(at(s.n, bestI), 'g', 6)));
|
||||
infoValRow_->setText(QStringLiteral("DataRow= %1").arg(QString::number(at(s.row, bestI), 'g', 6)));
|
||||
infoValPseu_->setText(
|
||||
QStringLiteral("Pseu_Resis= %1").arg(QString::number(at(s.pseu, bestI), 'g', 6)));
|
||||
for (QLabel* l : {infoValA_, infoValB_, infoValM_, infoValN_, infoValRow_, infoValPseu_})
|
||||
l->setVisible(true);
|
||||
infoPanel_->adjustSize();
|
||||
infoPanel_->move(plot_->canvas()->width() - infoPanel_->width() - 10, 10);
|
||||
infoPanel_->raise();
|
||||
|
|
@ -743,10 +776,10 @@ void RawDataChartView::buildMeasurementToolbar(const geopro::core::ScatterToolba
|
|||
|
||||
// [i] info + [▣] 框选:占位(暂未实现)。用 QPainter 画的线性图标(HiDPI 清晰,随主题)。
|
||||
auto* btnInfo = new QToolButton(toolbar);
|
||||
btnInfo->setToolTip(QStringLiteral("信息"));
|
||||
btnInfo->setToolTip(QStringLiteral("查看散点属性")); // 对照原版 datasetTool.vue tooltip
|
||||
styleToolIconButton(btnInfo, makeInfoIcon());
|
||||
auto* btnMarquee = new QToolButton(toolbar);
|
||||
btnMarquee->setToolTip(QStringLiteral("框选"));
|
||||
btnMarquee->setToolTip(QStringLiteral("散点的点选")); // 对照原版 datasetTool.vue tooltip
|
||||
styleToolIconButton(btnMarquee, makeMarqueeIcon());
|
||||
// 主题热切:重绘图标(info 锚定品牌蓝,marquee 描边随次要文本色)。
|
||||
connect(&ThemeManager::instance(), &ThemeManager::changed, btnInfo,
|
||||
|
|
@ -777,18 +810,30 @@ void RawDataChartView::buildMeasurementToolbar(const geopro::core::ScatterToolba
|
|||
connect(btnExport, &QPushButton::clicked, this, [this]() { exportDat(); });
|
||||
|
||||
// x / y 下拉:本地换列重绘;v 下拉:重新请求散点+色阶(M6);值类型下拉:本地变换(M7)。
|
||||
// 各下拉固定宽度对照原版 datasetTool.vue(X=120/Y=160/V=160/值类型=120)。
|
||||
xCombo_ = new QComboBox(toolbar);
|
||||
xCombo_->setFixedWidth(kComboW_X);
|
||||
fillCombo(xCombo_, conf.x, conf.defaultX, QString());
|
||||
yCombo_ = new QComboBox(toolbar);
|
||||
yCombo_->setFixedWidth(kComboW_Y);
|
||||
fillCombo(yCombo_, conf.y, conf.defaultY, QString());
|
||||
vCombo_ = new QComboBox(toolbar);
|
||||
vCombo_->setFixedWidth(kComboW_V);
|
||||
fillCombo(vCombo_, conf.v, conf.defaultV, QString());
|
||||
valueTypeCombo_ = new QComboBox(toolbar);
|
||||
valueTypeCombo_->setFixedWidth(kComboW_ValueType);
|
||||
// 值类型固定三项(原版 linearity/inverse/logarithm),本地变换无后端。
|
||||
valueTypeCombo_->addItem(QStringLiteral("线性"), QStringLiteral("linearity"));
|
||||
valueTypeCombo_->addItem(QStringLiteral("倒数"), QStringLiteral("inverse"));
|
||||
valueTypeCombo_->addItem(QStringLiteral("对数"), QStringLiteral("logarithm"));
|
||||
|
||||
// 无高程时禁用 X/Y 下拉(对照原版 :disabled="!currentHasElevation")。
|
||||
// 判断依据:高程相关备选列 altYElevationPseudo 非空即视为「有高程数据」(与 y 下拉
|
||||
// 「伪深度+高程」项的数据源一致;无高程时该列为空,X/Y 轴切换无意义故禁用)。
|
||||
const bool hasElevation = !data_.altYElevationPseudo.empty();
|
||||
xCombo_->setEnabled(hasElevation);
|
||||
yCombo_->setEnabled(hasElevation);
|
||||
|
||||
connect(xCombo_, QOverload<int>::of(&QComboBox::currentIndexChanged), this,
|
||||
[this](int) { replotForAxis(); });
|
||||
connect(yCombo_, QOverload<int>::of(&QComboBox::currentIndexChanged), this,
|
||||
|
|
@ -825,13 +870,17 @@ void RawDataChartView::buildMeasurementToolbar(const geopro::core::ScatterToolba
|
|||
tbLay->addWidget(btnShow);
|
||||
tbLay->addWidget(btnHide);
|
||||
tbLay->addWidget(btnFilter);
|
||||
tbLay->addWidget(btnExport);
|
||||
tbLay->addWidget(xCombo_);
|
||||
tbLay->addWidget(yCombo_);
|
||||
tbLay->addWidget(vCombo_);
|
||||
tbLay->addWidget(valueTypeCombo_);
|
||||
tbLay->addWidget(btnColorScale);
|
||||
tbLay->addStretch(); // 把主操作推到右侧
|
||||
tbLay->addStretch(); // 把导出 + 主操作推到右侧
|
||||
// 导出:原版在详情页头 Header(非工具条)。客户端页头「导出」HeaderAction 为跨 ddCode
|
||||
// 共用的静态占位(PanelHeader 不暴露按钮/不发信号,IDetailView 亦无导出接口),按 ddCode
|
||||
// 分派转发成本高且易误触其它视图;故 measurement 专属导出保留在工具条内,置于最右侧、
|
||||
// 紧邻主操作组,样式贴近原版(outline 风格的普通按钮)。
|
||||
tbLay->addWidget(btnExport);
|
||||
tbLay->addWidget(btnGen);
|
||||
tbLay->addWidget(btnInvert);
|
||||
tbLay->addWidget(btnSaveAs);
|
||||
|
|
|
|||
|
|
@ -103,7 +103,14 @@ private:
|
|||
// M13 [i]信息:信息模式开关 + 覆盖在图区右上的属性面板。
|
||||
bool infoMode_ = false;
|
||||
QWidget* infoPanel_ = nullptr; // 属性覆盖面板(A/B/M/N/DataRow/Pseu_Resis)
|
||||
QLabel* infoLabel_ = nullptr;
|
||||
QLabel* infoHint_ = nullptr; // 未点选时的提示文案
|
||||
// A/B/M/N 标签按原版配色(A 红 / B 蓝 / M 绿 / N 橙#F4B008);DataRow/Pseu_Resis 默认色。
|
||||
QLabel* infoValA_ = nullptr;
|
||||
QLabel* infoValB_ = nullptr;
|
||||
QLabel* infoValM_ = nullptr;
|
||||
QLabel* infoValN_ = nullptr;
|
||||
QLabel* infoValRow_ = nullptr;
|
||||
QLabel* infoValPseu_ = nullptr;
|
||||
|
||||
// 使用 unique_ptr 管理生命周期;attach 后 QwtPlot 接管绘制,但我们仍持有指针
|
||||
ColorMapService* colorSvc_ = nullptr; // heap,由 setData 重建
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@
|
|||
#include <QVBoxLayout>
|
||||
|
||||
#include "Theme.hpp"
|
||||
#include "ToastOverlay.hpp" // showToast:统一成功轻提示(规范 §7.7)
|
||||
#include "panels/chart/ScatterDataOps.hpp" // buildSaveRawDataBody(纯组装,便于单测)
|
||||
#include "repo/IDatasetCommandRepository.hpp"
|
||||
|
||||
|
|
@ -20,6 +21,7 @@ namespace geopro::app {
|
|||
|
||||
namespace {
|
||||
constexpr int kInversionW = 400; // 原版 inversion 另存为弹窗宽 400px
|
||||
constexpr int kRawDataW = 280; // 原版 RawData「数据另存为」弹窗宽 280px
|
||||
constexpr int kLabelW = 60; // 原版 .label width:60px
|
||||
} // namespace
|
||||
|
||||
|
|
@ -49,8 +51,9 @@ SaveAsDialog::SaveAsDialog(Mode mode, geopro::data::IDatasetCommandRepository* r
|
|||
nameRow->addWidget(nameEdit_, 1);
|
||||
root->addLayout(nameRow);
|
||||
} else {
|
||||
// ── RawData(measurement):新增/覆盖 + 名称(保持既有逻辑,不在本批返工范围)──
|
||||
// ── RawData(measurement):新增/覆盖 + 名称(对照原版「数据另存为」280px)──
|
||||
setWindowTitle(QStringLiteral("数据另存为"));
|
||||
setFixedWidth(kRawDataW);
|
||||
|
||||
auto* opLay = new QHBoxLayout();
|
||||
auto* rbNew = new QRadioButton(QStringLiteral("新增"), this);
|
||||
|
|
@ -114,6 +117,8 @@ void SaveAsDialog::onConfirm() {
|
|||
if (!self) return;
|
||||
self->okBtn_->setEnabled(true);
|
||||
if (ok) {
|
||||
// 成功提示挂到父窗口(对话框随即关闭,toast 取顶层窗口锚定,故用 parentWidget)。
|
||||
if (auto* anchor = self->parentWidget()) showToast(anchor, QStringLiteral("保存成功"));
|
||||
self->accept();
|
||||
} else {
|
||||
QMessageBox::warning(self, self->windowTitle(),
|
||||
|
|
|
|||
|
|
@ -87,4 +87,14 @@ int toggledDisplayStatus(int currentStatus) {
|
|||
return currentStatus == 0 ? 1 : 0; // 0 显示 → 1 隐藏;其余 → 0 显示
|
||||
}
|
||||
|
||||
int countScatterInRange(const std::vector<double>& v, double min, double max) {
|
||||
if (max < min) return 0;
|
||||
int count = 0;
|
||||
for (double x : v) {
|
||||
if (!std::isfinite(x)) continue; // 非有限值不计入(与直方图一致)
|
||||
if (x >= min && x <= max) ++count; // 闭区间 [min,max],与原版过滤一致
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
} // namespace geopro::app
|
||||
|
|
|
|||
|
|
@ -59,4 +59,8 @@ ScatterHistogram buildScatterHistogram(const std::vector<double>& v, double min,
|
|||
// 入参/返回 0=显示、1=隐藏。当前显示(0) → 隐藏(1);当前隐藏(非0) → 显示(0)。
|
||||
int toggledDisplayStatus(int currentStatus);
|
||||
|
||||
// 统计落在闭区间 [min,max] 内的有限值个数(M3 数据过滤「当前点数」/占比用)。
|
||||
// 非有限值(NaN/inf)不计入;max<min → 0。
|
||||
int countScatterInRange(const std::vector<double>& v, double min, double max);
|
||||
|
||||
} // namespace geopro::app
|
||||
|
|
|
|||
|
|
@ -1,18 +1,24 @@
|
|||
#include "panels/chart/ScatterFilterDialog.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <utility>
|
||||
|
||||
#include <QDoubleSpinBox>
|
||||
#include <QFormLayout>
|
||||
#include <QFrame>
|
||||
#include <QHBoxLayout>
|
||||
#include <QLabel>
|
||||
#include <QMessageBox>
|
||||
#include <QPointer>
|
||||
#include <QPushButton>
|
||||
#include <QSignalBlocker>
|
||||
#include <QVBoxLayout>
|
||||
|
||||
#include "FormKit.hpp"
|
||||
#include "panels/chart/ScatterDataOps.hpp" // buildScatterFilterBody
|
||||
#include "Theme.hpp"
|
||||
#include "ToastOverlay.hpp" // showToast:成功轻提示
|
||||
#include "panels/chart/RangeSlider.hpp"
|
||||
#include "panels/chart/ScatterDataOps.hpp" // buildScatterFilterBody / countScatterInRange
|
||||
#include "panels/chart/ScatterHistogram.hpp"
|
||||
#include "repo/IDatasetCommandRepository.hpp"
|
||||
|
||||
|
|
@ -20,6 +26,20 @@ namespace geopro::app {
|
|||
|
||||
namespace {
|
||||
constexpr double kSpinRange = 1e12; // 数值范围足够宽,覆盖电阻率/电位等量纲
|
||||
constexpr int kDialogW = 1000; // 原版 dataFilter.vue width:1000
|
||||
constexpr int kBodyH = 500; // 原版 .data-filter-container height:500
|
||||
constexpr int kInfoW = 300; // 原版 .filter-options width:300
|
||||
constexpr int kInfoLabelW = 120; // 原版 .label min-width:120
|
||||
const char* kHighlight = "#f77234"; // 原版橙色高亮(当前点数/原始点数)
|
||||
|
||||
// 信息区一行:定宽 label(左)+ 值(右)。返回值标签供调用方写值/上色。
|
||||
QLabel* addInfoRow(QFormLayout* form, const QString& labelText) {
|
||||
auto* lbl = new QLabel(labelText);
|
||||
lbl->setMinimumWidth(kInfoLabelW);
|
||||
auto* val = new QLabel();
|
||||
form->addRow(lbl, val);
|
||||
return val;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
ScatterFilterDialog::ScatterFilterDialog(geopro::data::IDatasetCommandRepository* repo,
|
||||
|
|
@ -31,43 +51,84 @@ ScatterFilterDialog::ScatterFilterDialog(geopro::data::IDatasetCommandRepository
|
|||
vFieldCode_(std::move(vFieldCode)) {
|
||||
setWindowTitle(QStringLiteral("数据过滤"));
|
||||
setModal(true);
|
||||
resize(640, 300);
|
||||
resize(kDialogW, kBodyH + 120); // body 500 + 滑块/按钮/边距
|
||||
|
||||
auto* root = formkit::dialogRoot(this);
|
||||
// 全量有限值 + 数据域 + 原始点数(统计基线)。
|
||||
values_.reserve(values.size());
|
||||
for (double x : values)
|
||||
if (std::isfinite(x)) values_.push_back(x);
|
||||
originalPoints_ = static_cast<int>(values_.size());
|
||||
if (!values_.empty()) {
|
||||
dataMin_ = *std::min_element(values_.begin(), values_.end());
|
||||
dataMax_ = *std::max_element(values_.begin(), values_.end());
|
||||
}
|
||||
|
||||
rangeLabel_ = new QLabel(QStringLiteral("数值范围:—"), this);
|
||||
root->addWidget(rangeLabel_);
|
||||
auto* root = new QVBoxLayout(this);
|
||||
root->setContentsMargins(space::kXl, space::kXl, space::kXl, space::kXl);
|
||||
root->setSpacing(space::kLg);
|
||||
|
||||
// 主体:左直方图 + 右范围输入(横向分栏,对照原版左图右控件布局)。
|
||||
// ── 上半区:左直方图 + 右信息区(高 500)──
|
||||
auto* bodyLay = new QHBoxLayout();
|
||||
bodyLay->setSpacing(space::kXl);
|
||||
histogram_ = new ScatterHistogramView(this);
|
||||
histogram_->setValues(values);
|
||||
histogram_->setValues(values_);
|
||||
histogram_->setMinimumHeight(kBodyH);
|
||||
bodyLay->addWidget(histogram_, 1);
|
||||
|
||||
auto* card = formkit::formCard(this);
|
||||
auto* cardLay = formkit::cardBody(card);
|
||||
// 右信息区(定宽 300,带边框卡片)。
|
||||
auto* info = new QFrame(this);
|
||||
info->setObjectName(QStringLiteral("filterInfo"));
|
||||
info->setFixedWidth(kInfoW);
|
||||
applyTokenizedStyleSheet(
|
||||
info, QStringLiteral("QFrame#filterInfo { border:1px solid {{border/default}};"
|
||||
" border-radius:4px; }"));
|
||||
auto* infoLay = new QVBoxLayout(info);
|
||||
infoLay->setContentsMargins(space::kXl, space::kXl, space::kXl, space::kXl);
|
||||
infoLay->setSpacing(space::kLg);
|
||||
|
||||
auto* form = formkit::makeEditForm();
|
||||
minSpin_ = new QDoubleSpinBox(this);
|
||||
minSpin_->setRange(-kSpinRange, kSpinRange);
|
||||
minSpin_->setDecimals(2);
|
||||
formkit::capField(minSpin_);
|
||||
auto* statForm = new QFormLayout();
|
||||
statForm->setLabelAlignment(Qt::AlignLeft);
|
||||
rangeValueLbl_ = addInfoRow(statForm, QStringLiteral("数值范围:"));
|
||||
percentLbl_ = addInfoRow(statForm, QStringLiteral("当前数据量占比:"));
|
||||
auto* origVal = addInfoRow(statForm, QStringLiteral("原始点数:"));
|
||||
origVal->setText(QString::number(originalPoints_));
|
||||
origVal->setStyleSheet(QStringLiteral("color:%1;").arg(kHighlight)); // 橙色高亮
|
||||
currentPtsLbl_ = addInfoRow(statForm, QStringLiteral("当前点数:"));
|
||||
currentPtsLbl_->setStyleSheet(QStringLiteral("color:%1;").arg(kHighlight)); // 橙色高亮
|
||||
infoLay->addLayout(statForm);
|
||||
|
||||
// 最大值在上、最小值在下(对照原版输入框顺序)。
|
||||
auto* inputForm = new QFormLayout();
|
||||
maxSpin_ = new QDoubleSpinBox(this);
|
||||
maxSpin_->setRange(-kSpinRange, kSpinRange);
|
||||
maxSpin_->setDecimals(2);
|
||||
formkit::capField(maxSpin_);
|
||||
form->addRow(formkit::editLabel(QStringLiteral("最小值")), minSpin_);
|
||||
form->addRow(formkit::editLabel(QStringLiteral("最大值")), maxSpin_);
|
||||
cardLay->addLayout(form);
|
||||
bodyLay->addWidget(card);
|
||||
root->addLayout(bodyLay);
|
||||
minSpin_ = new QDoubleSpinBox(this);
|
||||
minSpin_->setRange(-kSpinRange, kSpinRange);
|
||||
minSpin_->setDecimals(2);
|
||||
inputForm->addRow(new QLabel(QStringLiteral("最大值:")), maxSpin_);
|
||||
inputForm->addRow(new QLabel(QStringLiteral("最小值:")), minSpin_);
|
||||
infoLay->addLayout(inputForm);
|
||||
|
||||
// min/max 改动 → 实时同步直方图选区高亮(原版输入/滑块联动指示矩形)。
|
||||
connect(minSpin_, QOverload<double>::of(&QDoubleSpinBox::valueChanged), this,
|
||||
[this](double) { syncHistogramSel(); });
|
||||
connect(maxSpin_, QOverload<double>::of(&QDoubleSpinBox::valueChanged), this,
|
||||
[this](double) { syncHistogramSel(); });
|
||||
// 计算分布 / 重置(信息区中部,对照原版 .filter-actions)。
|
||||
auto* actionLay = new QHBoxLayout();
|
||||
auto* calcBtn = new QPushButton(QStringLiteral("计算分布"), this);
|
||||
auto* resetBtn = new QPushButton(QStringLiteral("重置"), this);
|
||||
actionLay->addStretch();
|
||||
actionLay->addWidget(calcBtn);
|
||||
actionLay->addWidget(resetBtn);
|
||||
infoLay->addLayout(actionLay);
|
||||
infoLay->addStretch();
|
||||
|
||||
bodyLay->addWidget(info);
|
||||
root->addLayout(bodyLay, 1);
|
||||
|
||||
// ── 底部:范围滑块 ──
|
||||
slider_ = new RangeSlider(this);
|
||||
slider_->setRange(dataMin_, dataMax_);
|
||||
slider_->setValues(dataMin_, dataMax_);
|
||||
root->addWidget(slider_);
|
||||
|
||||
// ── 底部按钮:取消 / 应用过滤(右对齐)──
|
||||
auto* btnLay = new QHBoxLayout();
|
||||
btnLay->addStretch();
|
||||
auto* cancelBtn = new QPushButton(QStringLiteral("取消"), this);
|
||||
|
|
@ -77,30 +138,69 @@ ScatterFilterDialog::ScatterFilterDialog(geopro::data::IDatasetCommandRepository
|
|||
btnLay->addWidget(applyBtn_);
|
||||
root->addLayout(btnLay);
|
||||
|
||||
// ── 三方联动(min/max 输入 ↔ 滑块 ↔ 直方图/统计)──
|
||||
connect(minSpin_, QOverload<double>::of(&QDoubleSpinBox::valueChanged), this,
|
||||
[this](double v) { setCurrentRange(v, maxSpin_->value(), false, true); });
|
||||
connect(maxSpin_, QOverload<double>::of(&QDoubleSpinBox::valueChanged), this,
|
||||
[this](double v) { setCurrentRange(minSpin_->value(), v, false, true); });
|
||||
connect(slider_, &RangeSlider::rangeChanged, this,
|
||||
[this](double lo, double hi) { setCurrentRange(lo, hi, true, false); });
|
||||
connect(calcBtn, &QPushButton::clicked, this, [this]() { refreshStats(); });
|
||||
connect(resetBtn, &QPushButton::clicked, this, &ScatterFilterDialog::onResetFilter);
|
||||
connect(cancelBtn, &QPushButton::clicked, this, &QDialog::reject);
|
||||
connect(applyBtn_, &QPushButton::clicked, this, &ScatterFilterDialog::onApply);
|
||||
|
||||
loadConfig();
|
||||
}
|
||||
|
||||
void ScatterFilterDialog::syncHistogramSel() {
|
||||
if (histogram_) histogram_->setSelection(minSpin_->value(), maxSpin_->value());
|
||||
void ScatterFilterDialog::setCurrentRange(double min, double max, bool fromSlider, bool fromSpin) {
|
||||
// 同步未发起方的控件(屏蔽信号避免回环),再刷新统计/直方图。
|
||||
if (!fromSpin) {
|
||||
const QSignalBlocker b1(minSpin_);
|
||||
const QSignalBlocker b2(maxSpin_);
|
||||
minSpin_->setValue(min);
|
||||
maxSpin_->setValue(max);
|
||||
}
|
||||
if (!fromSlider && slider_) {
|
||||
const QSignalBlocker b(slider_);
|
||||
slider_->setValues(min, max);
|
||||
}
|
||||
refreshStats();
|
||||
}
|
||||
|
||||
void ScatterFilterDialog::refreshStats() {
|
||||
const double mn = minSpin_->value();
|
||||
const double mx = maxSpin_->value();
|
||||
rangeValueLbl_->setText(QStringLiteral("%1 — %2")
|
||||
.arg(QString::number(mn, 'g', 6), QString::number(mx, 'g', 6)));
|
||||
const int cur = countScatterInRange(values_, mn, mx);
|
||||
currentPtsLbl_->setText(QString::number(cur));
|
||||
const QString pct = (originalPoints_ > 0)
|
||||
? QStringLiteral("%1%").arg(
|
||||
QString::number(100.0 * cur / originalPoints_, 'f', 2))
|
||||
: QStringLiteral("0%");
|
||||
percentLbl_->setText(pct);
|
||||
if (histogram_) histogram_->setSelection(mn, mx);
|
||||
}
|
||||
|
||||
void ScatterFilterDialog::onResetFilter() {
|
||||
// 重置:恢复到全量数据域(对照原版 resetFilter)。
|
||||
setCurrentRange(dataMin_, dataMax_, false, false);
|
||||
}
|
||||
|
||||
void ScatterFilterDialog::loadConfig() {
|
||||
if (!repo_) return;
|
||||
if (!repo_) {
|
||||
setCurrentRange(dataMin_, dataMax_, false, false); // 无仓储:用数据域初值
|
||||
return;
|
||||
}
|
||||
QPointer<ScatterFilterDialog> self(this);
|
||||
repo_->getScatterFilterConfig(
|
||||
dsObjectId_, vFieldCode_, [self](bool ok, QJsonObject cfg, QString) {
|
||||
if (!self || !ok) return;
|
||||
if (!self) return;
|
||||
if (!ok) { self->setCurrentRange(self->dataMin_, self->dataMax_, false, false); return; }
|
||||
const double mn = cfg.value(QStringLiteral("min")).toDouble();
|
||||
const double mx = cfg.value(QStringLiteral("max")).toDouble();
|
||||
self->minSpin_->setValue(mn);
|
||||
self->maxSpin_->setValue(mx);
|
||||
self->rangeLabel_->setText(QStringLiteral("数值范围:%1 — %2")
|
||||
.arg(QString::number(mn, 'g', 6),
|
||||
QString::number(mx, 'g', 6)));
|
||||
self->syncHistogramSel(); // 初值就位后同步直方图选区
|
||||
self->setCurrentRange(mn, mx, false, false);
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -120,8 +220,9 @@ void ScatterFilterDialog::onApply() {
|
|||
if (!self) return;
|
||||
self->applyBtn_->setEnabled(true);
|
||||
if (ok) {
|
||||
QMessageBox::information(self, self->windowTitle(),
|
||||
QStringLiteral("应用过滤成功!"));
|
||||
// 成功提示挂父窗口(对话框随即关闭)。文案对照原版「应用过滤成功!」。
|
||||
if (auto* anchor = self->parentWidget())
|
||||
showToast(anchor, QStringLiteral("应用过滤成功!"));
|
||||
self->accept();
|
||||
} else {
|
||||
QMessageBox::warning(self, self->windowTitle(),
|
||||
|
|
|
|||
|
|
@ -15,34 +15,47 @@ class IDatasetCommandRepository;
|
|||
namespace geopro::app {
|
||||
|
||||
class ScatterHistogramView;
|
||||
class RangeSlider;
|
||||
|
||||
// 「数据过滤」对话框(复刻原版 web dataFilter.vue):
|
||||
// 左侧数值分布直方图(自绘 ScatterHistogram,由打开方传入当前 V 值数组),
|
||||
// 右侧 min/max 输入框;改 min/max 时直方图同步高亮选区(对照原版 .filter-indicator)。
|
||||
// 打开时经 getScatterFilterConfig 取 min/max 初值填入输入框;
|
||||
// 「应用过滤」经 applyScatterFilter 生成过滤后数据集({sourceDsObjectId, sourceVFieldCode, min, max})。
|
||||
// 「数据过滤」对话框(1:1 复刻原版 web dataFilter.vue,弹窗宽 1000px):
|
||||
// 左:数值分布直方图(自绘 ScatterHistogram,hover 柱变红 + tooltip);
|
||||
// 右:信息区(数值范围 / 当前数据量占比 / 原始点数 / 当前点数橙色高亮 + 最大值/最小值输入 +
|
||||
// 「计算分布」「重置」);底部:范围双手柄滑块 + 「取消」「应用过滤」。
|
||||
// min/max 输入、滑块、直方图选区三方联动;统计随当前区间实时更新。
|
||||
// 打开时经 getScatterFilterConfig 取 min/max 初值;「应用过滤」经 applyScatterFilter
|
||||
// 生成过滤后数据集({sourceDsObjectId, sourceVFieldCode, min, max});成功提示 toast。
|
||||
// 回调用 QPointer 守卫(虽 modal exec,仍异步回调)。
|
||||
class ScatterFilterDialog : public QDialog {
|
||||
Q_OBJECT
|
||||
public:
|
||||
// values = 当前 V 值数组(与图上散点 v 同源,驱动直方图分布);可空(无值则直方图空态)。
|
||||
// values = 当前 V 值数组(与图上散点 v 同源,驱动直方图分布 + 统计);可空(空则空态)。
|
||||
ScatterFilterDialog(geopro::data::IDatasetCommandRepository* repo, QString dsObjectId,
|
||||
QString vFieldCode, std::vector<double> values, QWidget* parent = nullptr);
|
||||
|
||||
private:
|
||||
void loadConfig(); // 取 min/max 初值
|
||||
void onApply(); // 应用过滤
|
||||
void syncHistogramSel(); // 把当前 min/max 同步到直方图选区高亮
|
||||
void loadConfig(); // 取 min/max 初值
|
||||
void onApply(); // 应用过滤
|
||||
void onResetFilter(); // 重置:恢复到全量数据域
|
||||
void setCurrentRange(double min, double max, bool fromSlider, bool fromSpin); // 三方联动
|
||||
void refreshStats(); // 刷新统计(占比/当前点数)与直方图选区
|
||||
|
||||
geopro::data::IDatasetCommandRepository* repo_ = nullptr;
|
||||
QString dsObjectId_;
|
||||
QString vFieldCode_;
|
||||
|
||||
QLabel* rangeLabel_ = nullptr; // 「数值范围:min — max」
|
||||
std::vector<double> values_; // 全量有限 v 值(统计/分箱用)
|
||||
double dataMin_ = 0.0; // 数据域下界
|
||||
double dataMax_ = 0.0; // 数据域上界
|
||||
int originalPoints_ = 0; // 原始点数(全量有限值个数)
|
||||
|
||||
QLabel* rangeValueLbl_ = nullptr; // 「数值范围」值
|
||||
QLabel* percentLbl_ = nullptr; // 「当前数据量占比」值
|
||||
QLabel* currentPtsLbl_ = nullptr; // 「当前点数」值(橙色高亮)
|
||||
QDoubleSpinBox* minSpin_ = nullptr;
|
||||
QDoubleSpinBox* maxSpin_ = nullptr;
|
||||
QPushButton* applyBtn_ = nullptr;
|
||||
ScatterHistogramView* histogram_ = nullptr; // 左侧分布直方图
|
||||
RangeSlider* slider_ = nullptr; // 底部范围滑块
|
||||
};
|
||||
|
||||
} // namespace geopro::app
|
||||
|
|
|
|||
|
|
@ -3,8 +3,11 @@
|
|||
#include <algorithm>
|
||||
#include <cmath>
|
||||
|
||||
#include <QEvent>
|
||||
#include <QMouseEvent>
|
||||
#include <QPaintEvent>
|
||||
#include <QPainter>
|
||||
#include <QToolTip>
|
||||
|
||||
#include "panels/chart/ScatterDataOps.hpp" // buildScatterHistogram
|
||||
|
||||
|
|
@ -12,8 +15,9 @@ namespace geopro::app {
|
|||
|
||||
namespace {
|
||||
constexpr int kBinCount = 20; // 分箱数(对照原版 D3 stepRange=20)
|
||||
const QColor kBarIn(64, 128, 255); // 选区内柱:蓝(对照原版高亮)
|
||||
const QColor kBarIn(64, 128, 255); // 选区内柱:蓝(对照原版 #4080FF)
|
||||
const QColor kBarOut(200, 205, 215); // 选区外柱:灰
|
||||
const QColor kBarHover(245, 63, 63); // hover 柱:红(对照原版 #F53F3F)
|
||||
const QColor kIndicator(64, 128, 255, 51); // 选区指示矩形:rgba(64,128,255,0.2)
|
||||
const QColor kAxis(150, 150, 150); // 轴线/刻度
|
||||
constexpr int kPadL = 8; // 左右内边距
|
||||
|
|
@ -26,6 +30,7 @@ constexpr int kBarGap = 1; // 柱间距(像素)
|
|||
ScatterHistogramView::ScatterHistogramView(QWidget* parent) : QWidget(parent) {
|
||||
setMinimumHeight(160);
|
||||
setMinimumWidth(280);
|
||||
setMouseTracking(true); // 无按键也接收 MouseMove,用于 hover 高亮
|
||||
}
|
||||
|
||||
void ScatterHistogramView::setValues(const std::vector<double>& values) {
|
||||
|
|
@ -39,6 +44,7 @@ void ScatterHistogramView::setValues(const std::vector<double>& values) {
|
|||
dataMin_ = *std::min_element(values_.begin(), values_.end());
|
||||
dataMax_ = *std::max_element(values_.begin(), values_.end());
|
||||
}
|
||||
hoverBin_ = -1;
|
||||
update();
|
||||
}
|
||||
|
||||
|
|
@ -49,6 +55,54 @@ void ScatterHistogramView::setSelection(double min, double max) {
|
|||
update();
|
||||
}
|
||||
|
||||
int ScatterHistogramView::binAtX(int px) const {
|
||||
if (values_.empty() || !(dataMax_ > dataMin_)) return -1;
|
||||
const QRect r = rect();
|
||||
const int plotL = r.left() + kPadL;
|
||||
const int plotR = r.right() - kPadR;
|
||||
const int plotW = plotR - plotL;
|
||||
if (plotW <= 0) return -1;
|
||||
const double binW = static_cast<double>(plotW) / kBinCount;
|
||||
if (binW <= 0) return -1;
|
||||
const int idx = static_cast<int>((px - plotL) / binW);
|
||||
if (idx < 0 || idx >= kBinCount) return -1;
|
||||
return idx;
|
||||
}
|
||||
|
||||
void ScatterHistogramView::mouseMoveEvent(QMouseEvent* event) {
|
||||
const int bin = binAtX(event->position().toPoint().x());
|
||||
if (bin != hoverBin_) {
|
||||
hoverBin_ = bin;
|
||||
update();
|
||||
}
|
||||
if (bin >= 0 && dataMax_ > dataMin_) {
|
||||
// tooltip:数值范围 + 数据点数量(对照原版 D3 tooltip 两行)。
|
||||
const double step = (dataMax_ - dataMin_) / kBinCount;
|
||||
const double lo = dataMin_ + bin * step;
|
||||
const double hi = lo + step;
|
||||
const auto h = buildScatterHistogram(values_, dataMin_, dataMax_, kBinCount);
|
||||
const int cnt = (bin < static_cast<int>(h.counts.size())) ? h.counts[static_cast<std::size_t>(bin)] : 0;
|
||||
QToolTip::showText(event->globalPosition().toPoint(),
|
||||
QStringLiteral("数值范围: %1 - %2\n数据点数量: %3")
|
||||
.arg(QString::number(std::llround(lo)),
|
||||
QString::number(std::llround(hi)),
|
||||
QString::number(cnt)),
|
||||
this);
|
||||
} else {
|
||||
QToolTip::hideText();
|
||||
}
|
||||
QWidget::mouseMoveEvent(event);
|
||||
}
|
||||
|
||||
void ScatterHistogramView::leaveEvent(QEvent* event) {
|
||||
if (hoverBin_ != -1) {
|
||||
hoverBin_ = -1;
|
||||
update();
|
||||
}
|
||||
QToolTip::hideText();
|
||||
QWidget::leaveEvent(event);
|
||||
}
|
||||
|
||||
void ScatterHistogramView::paintEvent(QPaintEvent*) {
|
||||
QPainter p(this);
|
||||
p.setRenderHint(QPainter::Antialiasing, false);
|
||||
|
|
@ -93,7 +147,7 @@ void ScatterHistogramView::paintEvent(QPaintEvent*) {
|
|||
p.fillRect(ind, kIndicator);
|
||||
}
|
||||
|
||||
// 画柱:高度按计数归一;选区内蓝、选区外灰。
|
||||
// 画柱:高度按计数归一;hover 柱红;其余按选区内蓝/外灰。
|
||||
const double binW = static_cast<double>(plotW) / n;
|
||||
for (int i = 0; i < n; ++i) {
|
||||
const double binLo = dataMin_ + i * h.step;
|
||||
|
|
@ -102,10 +156,11 @@ void ScatterHistogramView::paintEvent(QPaintEvent*) {
|
|||
maxCount * plotH);
|
||||
const int bx = static_cast<int>(plotL + i * binW) + kBarGap;
|
||||
const int bw = std::max(1, static_cast<int>(binW) - 2 * kBarGap);
|
||||
// 柱中心落在选区内 → 高亮(与原版“区间内点高亮”观感一致)。
|
||||
// 柱中心落在选区内 → 高亮蓝(与原版“区间内点高亮”观感一致);hover 优先红。
|
||||
const double binCenter = (binLo + binHi) / 2.0;
|
||||
const bool inSel = hasSel_ && binCenter >= selMin_ && binCenter <= selMax_;
|
||||
p.fillRect(bx, plotBottom - barH, bw, barH, inSel ? kBarIn : kBarOut);
|
||||
const QColor c = (i == hoverBin_) ? kBarHover : (inSel ? kBarIn : kBarOut);
|
||||
p.fillRect(bx, plotBottom - barH, bw, barH, c);
|
||||
}
|
||||
|
||||
// 底部基线 + 两端数值刻度(min/max)。
|
||||
|
|
|
|||
|
|
@ -8,7 +8,8 @@ namespace geopro::app {
|
|||
// 数值分布直方图(M3 数据过滤对话框左侧分布图,复刻原版 dataFilter.vue 的 D3 直方图)。
|
||||
// 自绘 QWidget:按全量 v 值的数据域分箱画柱;当前选定 [min,max] 区间内的柱高亮蓝、区间外灰,
|
||||
// 并在选区上叠加半透明蓝色指示矩形(对照原版 .filter-indicator)。x 轴底部画数值刻度。
|
||||
// 选区由对话框输入框/范围联动 setSelection 更新(本控件只读展示,不发选择信号)。
|
||||
// 选区由对话框输入框/范围联动 setSelection 更新。
|
||||
// hover:鼠标悬停某柱时该柱变红(对照原版 #F53F3F)+ QToolTip 显示「数值范围 / 数据点数量」。
|
||||
// 命名加 View 后缀以与 ScatterDataOps.hpp 的数据结构 struct ScatterHistogram(分箱结果)区分,
|
||||
// 避免同名 geopro::app::ScatterHistogram 在同一 TU 内冲突。
|
||||
class ScatterHistogramView : public QWidget {
|
||||
|
|
@ -23,14 +24,20 @@ public:
|
|||
|
||||
protected:
|
||||
void paintEvent(QPaintEvent* event) override;
|
||||
void mouseMoveEvent(QMouseEvent* event) override; // hover 高亮 + tooltip
|
||||
void leaveEvent(QEvent* event) override; // 离开清 hover
|
||||
|
||||
private:
|
||||
// 命中坐标 x(像素)所在的柱索引;未命中返回 -1。绘制/命中共用同一映射,保证一致。
|
||||
int binAtX(int px) const;
|
||||
|
||||
std::vector<double> values_; // 全量有限 v 值(已过滤 NaN/inf)
|
||||
double dataMin_ = 0.0; // 数据域下界(分箱总区间)
|
||||
double dataMax_ = 0.0; // 数据域上界
|
||||
double selMin_ = 0.0; // 当前选区下界
|
||||
double selMax_ = 0.0; // 当前选区上界
|
||||
bool hasSel_ = false; // 选区有效(selMin<=selMax)
|
||||
int hoverBin_ = -1; // 当前 hover 的柱索引(-1=无)
|
||||
};
|
||||
|
||||
} // namespace geopro::app
|
||||
|
|
|
|||
|
|
@ -122,3 +122,12 @@ TEST(ScatterDataOps, HistogramDegenerateReturnsEmpty) {
|
|||
EXPECT_TRUE(buildScatterHistogram(v, 5.0, 5.0, 10).counts.empty()); // min==max
|
||||
EXPECT_TRUE(buildScatterHistogram(v, 0.0, 10.0, 0).counts.empty()); // binCount<=0
|
||||
}
|
||||
|
||||
TEST(ScatterDataOps, CountInRangeClosedInterval) {
|
||||
std::vector<double> v{0, 5, 10, 15, -3, std::nan("")};
|
||||
// 闭区间 [0,10]:0,5,10 计入;15、-3 区间外;NaN 跳过。
|
||||
EXPECT_EQ(countScatterInRange(v, 0.0, 10.0), 3);
|
||||
EXPECT_EQ(countScatterInRange(v, 5.0, 5.0), 1); // 单点闭区间
|
||||
EXPECT_EQ(countScatterInRange(v, 10.0, 0.0), 0); // max<min → 0
|
||||
EXPECT_EQ(countScatterInRange(v, 100.0, 200.0), 0); // 全在区间外
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue