geopro/src/app/panels/chart/ScatterHistogram.cpp

176 lines
6.7 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#include "panels/chart/ScatterHistogram.hpp"
#include <algorithm>
#include <cmath>
#include <QEvent>
#include <QMouseEvent>
#include <QPaintEvent>
#include <QPainter>
#include <QToolTip>
#include "panels/chart/ScatterDataOps.hpp" // buildScatterHistogram
namespace geopro::app {
namespace {
constexpr int kBinCount = 20; // 分箱数(对照原版 D3 stepRange=20
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; // 左右内边距
constexpr int kPadR = 8;
constexpr int kPadTop = 8; // 顶部内边距
constexpr int kAxisH = 18; // 底部刻度区高度
constexpr int kBarGap = 1; // 柱间距(像素)
} // namespace
ScatterHistogramView::ScatterHistogramView(QWidget* parent) : QWidget(parent) {
setMinimumHeight(160);
setMinimumWidth(280);
setMouseTracking(true); // 无按键也接收 MouseMove用于 hover 高亮
}
void ScatterHistogramView::setValues(const std::vector<double>& values) {
values_.clear();
values_.reserve(values.size());
for (double x : values)
if (std::isfinite(x)) values_.push_back(x);
if (values_.empty()) {
dataMin_ = dataMax_ = 0.0;
} else {
dataMin_ = *std::min_element(values_.begin(), values_.end());
dataMax_ = *std::max_element(values_.begin(), values_.end());
}
hoverBin_ = -1;
update();
}
void ScatterHistogramView::setSelection(double min, double max) {
selMin_ = min;
selMax_ = max;
hasSel_ = (min <= 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);
const QRect r = rect();
const int plotL = r.left() + kPadL;
const int plotR = r.right() - kPadR;
const int plotTop = r.top() + kPadTop;
const int plotBottom = r.bottom() - kAxisH;
const int plotW = plotR - plotL;
const int plotH = plotBottom - plotTop;
if (plotW <= 0 || plotH <= 0) return;
// 数据域无效(无值/退化区间)→ 仅画基线,空态。
if (values_.empty() || !(dataMax_ > dataMin_)) {
p.setPen(kAxis);
p.drawLine(plotL, plotBottom, plotR, plotBottom);
return;
}
// 在全量数据域上分箱(每柱代表一个等宽区间)。
const auto h = buildScatterHistogram(values_, dataMin_, dataMax_, kBinCount);
const int n = static_cast<int>(h.counts.size());
if (n <= 0) return;
const int maxCount = *std::max_element(h.counts.begin(), h.counts.end());
if (maxCount <= 0) {
p.setPen(kAxis);
p.drawLine(plotL, plotBottom, plotR, plotBottom);
return;
}
// 值 → 像素 x 的映射(数据域 [dataMin,dataMax] → [plotL,plotR])。
auto xPix = [&](double val) {
return plotL + (val - dataMin_) / (dataMax_ - dataMin_) * plotW;
};
// 选区指示矩形(先画,柱叠其上)。
if (hasSel_ && selMax_ > selMin_) {
const double lo = std::clamp(selMin_, dataMin_, dataMax_);
const double hi = std::clamp(selMax_, dataMin_, dataMax_);
const QRectF ind(xPix(lo), plotTop, xPix(hi) - xPix(lo), plotH);
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;
const double binHi = binLo + h.step;
const int barH = static_cast<int>(static_cast<double>(h.counts[static_cast<std::size_t>(i)]) /
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_;
const QColor c = (i == hoverBin_) ? kBarHover : (inSel ? kBarIn : kBarOut);
p.fillRect(bx, plotBottom - barH, bw, barH, c);
}
// 底部基线 + 两端数值刻度min/max
p.setPen(kAxis);
p.drawLine(plotL, plotBottom, plotR, plotBottom);
p.drawText(QRect(plotL, plotBottom, plotW / 2, kAxisH), Qt::AlignLeft | Qt::AlignVCenter,
QString::number(dataMin_, 'g', 4));
p.drawText(QRect(plotL + plotW / 2, plotBottom, plotW / 2, kAxisH),
Qt::AlignRight | Qt::AlignVCenter, QString::number(dataMax_, 'g', 4));
}
} // namespace geopro::app