#include "panels/chart/ScatterHistogram.hpp" #include #include #include #include #include #include #include #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& 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(plotW) / kBinCount; if (binW <= 0) return -1; const int idx = static_cast((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(h.counts.size())) ? h.counts[static_cast(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(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(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(static_cast(h.counts[static_cast(i)]) / maxCount * plotH); const int bx = static_cast(plotL + i * binW) + kBarGap; const int bw = std::max(1, static_cast(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