一、简介
通过QCustomPlot实现ColorBar,直观显示各个位置的异常情况。实现效果如下,
二、源码
- CPColorBar.hpp
// CPColorBar.hpp
#pragma once
#include "qcustomplot.h"
#include <QHash>
class QCP_LIB_DECL CPColorBarData {
public:
CPColorBarData() : key(0), value(0) {}
CPColorBarData(double key, double value) : key(key), value(value) {}
inline double sortKey() const {
return key;
}
inline static CPColorBarData fromSortKey(double sortKey) {
return CPColorBarData{sortKey, 0};
}
inline static bool sortKeyIsMainKey() {
return true;
}
inline double mainKey() const {
return key;
}
inline double mainValue() const {
return value;
}
inline QCPRange valueRange() const {
return QCPRange{value, value};
}
double key, value;
};
Q_DECLARE_TYPEINFO(CPColorBarData, Q_PRIMITIVE_TYPE);
typedef QCPDataContainer<CPColorBarData> QCPColorBarDataContainer;
class QCP_LIB_DECL CPColorBar : public QCPAbstractPlottable1D<CPColorBarData> {
Q_OBJECT
Q_PROPERTY(double width READ width WRITE setWidth)
public:
explicit CPColorBar(QCPAxis* keyAxis, QCPAxis* valueAxis);
virtual ~CPColorBar() Q_DECL_OVERRIDE;
// getters
double width() const {
return mWidth;
}
QSharedPointer<QCPColorBarDataContainer> data() const {
return mDataContainer;
}
// setters
void setData(QSharedPointer<QCPColorBarDataContainer> data);
void setData(const QVector<double>& keys, const QVector<double>& values, bool alreadySorted = false);
void setWidth(double width);
void setColor(const QHash<int, QColor>& color);
// 非属性方法
void addData(const QVector<double>& keys, const QVector<double>& values, bool alreadySorted = false) const;
void addData(double key, double value);
// 重新实现的虚方法
virtual QCPDataSelection selectTestRect(const QRectF& rect, bool onlySelectable) const Q_DECL_OVERRIDE;
virtual double selectTest(
const QPointF& pos, bool onlySelectable, QVariant* details = nullptr) const Q_DECL_OVERRIDE;
virtual QCPRange getKeyRange(bool& foundRange, QCP::SignDomain inSignDomain = QCP::sdBoth) const Q_DECL_OVERRIDE;
virtual QCPRange getValueRange(bool& foundRange, QCP::SignDomain inSignDomain = QCP::sdBoth,
const QCPRange& inKeyRange = QCPRange()) const Q_DECL_OVERRIDE;
virtual QPointF dataPixelPosition(int index) const Q_DECL_OVERRIDE;
protected:
// 属性成员
double mWidth;
QHash<int, QColor> mColor;
virtual void draw(QCPPainter* painter) Q_DECL_OVERRIDE;
virtual void drawLegendIcon(QCPPainter* painter, const QRectF& rect) const Q_DECL_OVERRIDE;
void getVisibleDataBounds(
QCPColorBarDataContainer::const_iterator& begin, QCPColorBarDataContainer::const_iterator& end) const;
friend class QCustomPlot;
private:
void getOptimizedBarData(QVector<CPColorBarData>* barData, const QCPColorBarDataContainer::const_iterator& begin,
const QCPColorBarDataContainer::const_iterator& end) const;
bool mAdaptiveSampling; // 自适应采样
};
- CPColorBar.cpp
// CPColorBar.cpp
#include "CPColorBar.hpp"
#include <algorithm>
#include <limits>
CPColorBar::CPColorBar(QCPAxis* keyAxis, QCPAxis* valueAxis)
: QCPAbstractPlottable1D<CPColorBarData>(keyAxis, valueAxis), mWidth{1}, mAdaptiveSampling{true} {
// 修改从抽象绘图表继承的属性
mPen.setColor(Qt::blue);
mPen.setStyle(Qt::SolidLine);
mBrush.setColor(QColor(40, 50, 255, 30));
mBrush.setStyle(Qt::SolidPattern);
mSelectionDecorator->setBrush(QBrush(QColor(160, 160, 255)));
}
CPColorBar::~CPColorBar() = default;
void CPColorBar::setData(QSharedPointer<QCPColorBarDataContainer> data) {
mDataContainer = data;
}
void CPColorBar::setData(const QVector<double>& keys, const QVector<double>& values, bool alreadySorted) {
mDataContainer->clear();
addData(keys, values, alreadySorted);
}
// 设置条的宽度
void CPColorBar::setWidth(double width) {
mWidth = width;
}
void CPColorBar::setColor(const QHash<int, QColor>& color) {
mColor = color;
}
void CPColorBar::addData(const QVector<double>& keys, const QVector<double>& values, bool alreadySorted)const {
if (keys.size() != values.size())
qDebug() << Q_FUNC_INFO << "keys and values have different sizes:" << keys.size() << values.size();
const int n = qMin(keys.size(), values.size());
QVector<CPColorBarData> tempData(n);
QVector<CPColorBarData>::iterator it = tempData.begin();
const QVector<CPColorBarData>::iterator itEnd = tempData.end();
int i = 0;
while (it != itEnd) {
it->key = keys[i];
it->value = values[i];
++it;
++i;
}
mDataContainer->add(tempData, alreadySorted); // 请勿修改 tempData 以防止写入时复制
}
void CPColorBar::addData(double key, double value) {
mDataContainer->add(CPColorBarData(key, value));
}
QCPDataSelection CPColorBar::selectTestRect(const QRectF& rect, bool onlySelectable) const {
QCPDataSelection result;
return result;
}
double CPColorBar::selectTest(const QPointF& pos, bool onlySelectable, QVariant* details) const {
return -1;
}
QCPRange CPColorBar::getKeyRange(bool& foundRange, QCP::SignDomain inSignDomain) const {
return mDataContainer->keyRange(foundRange, inSignDomain);
}
QCPRange CPColorBar::getValueRange(bool& foundRange, QCP::SignDomain inSignDomain, const QCPRange& inKeyRange) const {
return mDataContainer->valueRange(foundRange, inSignDomain, inKeyRange);
}
QPointF CPColorBar::dataPixelPosition(int index) const {
if (index >= 0 && index < mDataContainer->size()) {
QCPAxis* keyAxis = mKeyAxis.data();
QCPAxis* valueAxis = mValueAxis.data();
if (!keyAxis || !valueAxis) {
qDebug() << Q_FUNC_INFO << "invalid key or value axis";
return {};
}
const QCPDataContainer<CPColorBarData>::const_iterator it = mDataContainer->constBegin() + index;
const double valuePixel = valueAxis->coordToPixel(it->value);
const double keyPixel = keyAxis->coordToPixel(it->key);
if (keyAxis->orientation() == Qt::Horizontal)
return {keyPixel, valuePixel};
else
return {valuePixel, keyPixel};
} else {
qDebug() << Q_FUNC_INFO << "Index out of bounds" << index;
return {};
}
}
/* 从基类继承文档 */
void CPColorBar::draw(QCPPainter* painter) {
if (!mKeyAxis || !mValueAxis) {
qDebug() << Q_FUNC_INFO << "invalid key or value axis";
return;
}
if (mDataContainer->isEmpty())
return;
QCPColorBarDataContainer::const_iterator begin, end;
getVisibleDataBounds(begin, end);
// 绘制片段
mDataContainer->limitIteratorsToDataRange(begin, end, QCPDataRange(0, dataCount()));
if (begin == end || begin + 1 == end)
return;
QVector<CPColorBarData> barData;
getOptimizedBarData(&barData, begin, end);
if (barData.empty())
return;
begin = barData.constBegin();
end = barData.constEnd();
QCPAxis* keyAxis = mKeyAxis.data();
QCPAxis* valueAxis = mValueAxis.data();
const QCPRange valueRange = valueAxis->range();
const int top = valueAxis->coordToPixel(valueRange.upper);
const int bottom = valueAxis->coordToPixel(valueRange.lower);
double currentStartKey{begin->key};
int currentStartValue{static_cast<int>(begin->value)};
for (QCPColorBarDataContainer::const_iterator it = begin; it != end; ++it) {
// 如果设置了标志,则检查数据有效性:
#ifdef QCUSTOMPLOT_CHECK_DATA
if (QCP::isInvalidData(it->key, it->value))
qDebug() << Q_FUNC_INFO << "Data point at" << it->key << "of drawn range invalid."
<< "Plottable name:" << name();
#endif
const bool value_changed = static_cast<int>(it->value) != currentStartValue;
if (value_changed || it + 1 == end) {
const double endMiddleKey = value_changed ? ((it - 1)->key + it->key) * 0.5 : it->key; // 取跃变的中间位置
// 着色绘制
const int startPix = keyAxis->coordToPixel(currentStartKey);
const int endPix = keyAxis->coordToPixel(endMiddleKey);
if (endPix - startPix >= 1) {
const auto iter = mColor.find(currentStartValue);
const QColor color = iter != mColor.cend() ? iter.value() : Qt::black;
painter->setPen(color);
painter->setBrush(QBrush(color));
painter->drawRect(startPix, top, endPix - startPix, bottom - top);
}
currentStartKey = endMiddleKey;
currentStartValue = static_cast<int>(it->value);
if (value_changed && it + 1 == end) {
--it;
}
}
}
// 绘制不只是线条/散点笔和画笔的其他选择装饰
if (mSelectionDecorator)
mSelectionDecorator->drawDecoration(painter, selection());
}
void CPColorBar::getOptimizedBarData(QVector<CPColorBarData>* barData,
const QCPColorBarDataContainer::const_iterator& begin, const QCPColorBarDataContainer::const_iterator& end) const {
if (!barData)
return;
QCPAxis* keyAxis = mKeyAxis.data();
QCPAxis* valueAxis = mValueAxis.data();
if (!keyAxis || !valueAxis) {
qDebug() << Q_FUNC_INFO << "invalid key or value axis";
return;
}
if (begin == end)
return;
const int dataCount = static_cast<int>(end - begin);
int maxCount = (std::numeric_limits<int>::max)();
if (mAdaptiveSampling) {
// 所选key区间所对应的像素宽度
const double keyPixelSpan = qAbs(keyAxis->coordToPixel(begin->key) - keyAxis->coordToPixel((end - 1)->key));
if (2 * keyPixelSpan + 2 < static_cast<double>((std::numeric_limits<int>::max)()))
maxCount = static_cast<int>(2 * keyPixelSpan + 2); // 最大点数为区间像素的两倍
}
barData->reserve(maxCount * 1.05);
if (mAdaptiveSampling && dataCount >= maxCount) { // 仅当平均每个像素至少有两个点时才使用自适应采样
QCPColorBarDataContainer::const_iterator it = begin;
double maxValue = it->value;
QCPColorBarDataContainer::const_iterator currentIntervalFirstPoint = it;
const int reversedFactor = keyAxis->pixelOrientation(); // 用于计算 keyEpsilon 像素到正确的方向
const int reversedRound = reversedFactor == -1 ? 1 : 0;
double currentIntervalStartKey =
keyAxis->pixelToCoord(static_cast<int>(keyAxis->coordToPixel(begin->key) + reversedRound));
double lastIntervalEndKey = currentIntervalStartKey;
// 映射到绘图键坐标时屏幕上一个像素的间隔
double keyEpsilon =
qAbs(currentIntervalStartKey
- keyAxis->pixelToCoord(keyAxis->coordToPixel(currentIntervalStartKey) + 1.0 * reversedFactor));
// 指示是否需要在每个间隔后更新 keyEpsilon(对于对数轴)
const bool keyEpsilonVariable = keyAxis->scaleType() == QCPAxis::stLogarithmic;
int intervalDataCount = 1;
++it; // 将迭代器提前到第二个数据点,因为自适应采样在 1 点回溯中起作用
while (it != end) {
// 数据点仍在同一像素内,因此请跳过它并在必要时扩展此集群的值范围
if (it->key < currentIntervalStartKey + keyEpsilon) {
if (it->value > maxValue)
maxValue = it->value;
++intervalDataCount;
} else { // 新的像素间隔开始
if (intervalDataCount >= 2) { // 最后一个像素有多个数据点,将它们合并到一个集群中
// 最后一个点更远,所以这个集群的第一个点必须是一个真实的数据点
if (lastIntervalEndKey < currentIntervalStartKey - keyEpsilon)
barData->append(CPColorBarData(
currentIntervalStartKey + keyEpsilon * 0.2, currentIntervalFirstPoint->value));
barData->append(CPColorBarData(currentIntervalStartKey + keyEpsilon * 0.75, maxValue));
// 新像素开始远离前一个集群,因此请确保集群的最后一个点位于真实数据点
if (it->key > currentIntervalStartKey + keyEpsilon * 2)
barData->append(CPColorBarData(currentIntervalStartKey + keyEpsilon * 0.8, (it - 1)->value));
} else
barData->append(CPColorBarData(currentIntervalFirstPoint->key, currentIntervalFirstPoint->value));
lastIntervalEndKey = (it - 1)->key;
maxValue = it->value;
currentIntervalFirstPoint = it;
currentIntervalStartKey =
keyAxis->pixelToCoord(static_cast<int>(keyAxis->coordToPixel(it->key) + reversedRound));
if (keyEpsilonVariable)
keyEpsilon = qAbs(
currentIntervalStartKey
- keyAxis->pixelToCoord(keyAxis->coordToPixel(currentIntervalStartKey) + 1.0 * reversedFactor));
intervalDataCount = 1;
}
++it;
}
// 处理最后一个间隔
if (intervalDataCount >= 2) { // 最后一个像素有多个数据点,将它们合并到一个集群中
// 最后一个点不是一个集群,所以这个集群的第一个点必须是一个真实的数据点
if (lastIntervalEndKey < currentIntervalStartKey - keyEpsilon)
barData->append(
CPColorBarData(currentIntervalStartKey + keyEpsilon * 0.2, currentIntervalFirstPoint->value));
barData->append(CPColorBarData(currentIntervalStartKey + keyEpsilon * 0.75, maxValue));
} else
barData->append(CPColorBarData(currentIntervalFirstPoint->key, currentIntervalFirstPoint->value));
} else { // 不使用自适应采样算法,将点从数据容器一对一传输到输出
barData->resize(dataCount);
std::copy(begin, end, barData->begin());
}
}
/* 从基类继承文档 */
void CPColorBar::drawLegendIcon(QCPPainter* painter, const QRectF& rect) const {
// 绘制填充矩形:
applyDefaultAntialiasingHint(painter);
painter->setBrush(mBrush);
painter->setPen(mPen);
QRectF r = QRectF(0, 0, rect.width() * 0.67, rect.height() * 0.67);
r.moveCenter(rect.center());
painter->drawRect(r);
}
/*! 内部的
由draw调用以确定在当前键轴范围设置下哪个数据(键)范围可见,因此只需要处理。 它还考虑了条形宽度。
begin 返回一个迭代器,指向绘图时需要考虑的最低数据点。 请注意,为了得到一个干净的绘图,
一直到轴矩形的边缘,较低的可能仍然刚好在可见范围之外。
end 返回一个比最高可见数据点高一的迭代器。 和以前一样,end 也可能位于可见范围之外。
如果绘图表不包含数据,则起点和终点都指向 constEnd。
*/
void CPColorBar::getVisibleDataBounds(
QCPColorBarDataContainer::const_iterator& begin, QCPColorBarDataContainer::const_iterator& end) const {
if (!mKeyAxis) {
qDebug() << Q_FUNC_INFO << "invalid key axis";
begin = mDataContainer->constEnd();
end = mDataContainer->constEnd();
return;
}
if (mDataContainer->isEmpty()) {
begin = mDataContainer->constEnd();
end = mDataContainer->constEnd();
return;
}
QCPAxis* keyAxis = mKeyAxis.data();
QCPAxis* valueAxis = mValueAxis.data();
// 获取可见数据范围
begin = mDataContainer->findBegin(keyAxis->range().lower);
end = mDataContainer->findEnd(keyAxis->range().upper);
// 将下限/上限限制为 rangeRestriction
// mDataContainer->limitIteratorsToDataRange(begin, end, QCPDataRange(0, dataCount()));
}
三、示例
#include "CPColorBar.hpp"
#include <QApplication>
int main(int argc, char* argv[]) {
QApplication a(argc, argv);
// 生成数据
QVector<double> x{
-1, -0.9, -0.8, -0.7, -0.6, -0.5, -0.4, -0.3, -0.2, -0.1, 0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0};
QVector<double> y{0, 3, 2, 1, 0, 0, 3, 0, 3, 0, 2, 2, 2, 0, 0, 1, 0, 1, 0, 1, 0};
// 创建图表
QCustomPlot* customPlot = new QCustomPlot();
auto* color_bar = new CPColorBar(customPlot->xAxis, customPlot->yAxis);
color_bar->setColor({{0, Qt::gray}, {1, Qt::blue}, {2, Qt::yellow}, {3, Qt::red}});
color_bar->setData(x, y);
// 设置坐标轴范围
customPlot->xAxis->setRange(-1, 1);
customPlot->yAxis->setRange(-1, 1);
customPlot->yAxis->setVisible(false);
customPlot->replot();
customPlot->show();
customPlot->resize(600, 100);
return a.exec();
}
运行效果如下: