Автоматическая подстройка диапазона оси значений QValueAxis на гистограммах Qt Charts
Часто в разработке реальных приложений точный диапазон значений, которые необходимо вывести на гистограмму, бывает заранее неизвестен. Максимальные ожидаемые значения могут различаться в десятки и сотни раз. Поэтому установка шкал значений на гистограмме в какое-то предельно максимальное значение будет не лучшим решением – пострадает наглядность представления данных при выводе малых значений, по крайней мере, до выполнения пользователем ручной настройки шкалы.
В данной статье рассматривается, как автоматически изменять диапазон оси значений QValueAxis
на гистограмме Qt Charts, исходя из значений, принимаемых наборами полос (объектами QBarset
) из модели данных.
Пример автоматической подстройки диапазона оси значений QValueAxis
В данном примере на гистограмме выводятся получаемые из модели BitrateTableModel
данные о скоростях элементарных потоков (в кбит/с).
Элементарные потоки со значениями PID, заканчивающимися на единицу, – это потоки видео. В момент запуска приложения их скорости составляют 2650-2750 кбит/с, а через 5 секунд после запуска их скорости увеличиваются до 7600-7700 кбит/с. Подстройка шкалы оси значений происходит в первый раз в момент запуска приложения и второй раз в момент изменения скоростей видеопотоков.
Создание диаграммы
Для простоты всё создание диаграммы выполним в конструкторе объекта класса, который наследуется от QMainWindow
.
Процесс создания гистограммы практически аналогичен примеру, приведенному в статье «Использование гистограмм Qt Charts совместно с моделями данных», за исключением того, что гистограмма данной статье будет использоваться горизонтальная, плюс добавилась обработка выводимых на ней значений. Поэтому некоторые подробности будут опущены.
Начнем с создания экземпляра класса BitrateTableModel
. Класс BitrateTableModel
является производным от QAbstractTableModel
и был создан только для этого примера. Конструктор этого класса заполняет внутреннее хранилище данных модели данными, необходимыми для нашего примера работы с диаграммами. А после создания модель каждые 500 мс обновляет данные о скоростях элементарных потоков псевдослучайными значениями.
model = new BitrateTableModel;
Данные мы хотим отображать не только на гистограмме, но и в таблице (объекте QTableView
).
ui->tableView->setModel(model);
ui->tableView->horizontalHeader()->setSectionResizeMode(QHeaderView::Stretch);
model->setParent(ui->tableView);
Теперь нам нужен экземпляр QChart
для отображения на гистограмме тех же данных.
chart = new QChart;
Создаем новый объект последовательности полос QHorizontalBarSeries
. Следующие три строки создают преобразователь данных, экземпляр класса QVBarModelMapper
, и указывают, что данные для наборов полос должны быть взяты из столбца модели с индексом 1 (значение BitrateTableModel::ColumnBitrate
). Чтобы создать связь между последовательностью полос и моделью, передаем оба этих объекта преобразователю данных, т.е. объекту mapper
класса QVBarModelMapper
.
Наконец, добавляем к диаграмме последовательность полос series
.
QHorizontalBarSeries *series = new QHorizontalBarSeries;
mapper = new QVBarModelMapper(this);
mapper->setFirstBarSetColumn(BitrateTableModel::ColumnBitrate);
mapper->setLastBarSetColumn(BitrateTableModel::ColumnBitrate);
mapper->setSeries(series);
mapper->setModel(model);
chart->addSeries(series);
Также неплохо было бы, чтобы на оси диаграммы были подписаны категории, которые описывали бы суть данных. Категории в данном примере – это значения идентификаторов пакетов (PID) элементарных потоков, хранящиеся в модели BitrateTableModel
в столбце 0 (значение BitrateTableModel::ColumnPid
). Следующий цикл сохраняет значения категорий в объект QStringList categories
, плюс определяет максимальное текущее значение скорости, которое необходимо отобразить на гистограмме.
int rowCount = model->rowCount();
QStringList categories;
qreal maxValue = 0;
for(int i = 0; i < rowCount; ++i)
{
QModelIndex idx = model->index(i, BitrateTableModel::ColumnPid);
categories << model->data(idx).toString();
idx = model->index(i, BitrateTableModel::ColumnBitrate);
qreal curValue = model->data(idx).toDouble();
if (curValue > maxValue)
maxValue = curValue;
}
Создаем ось категорий (объект QBarCategoryAxis
). Делаем ее линии сетки невидимыми и передаем ей сформированный список категорий. После чего добавляем ее на диаграмму (в этом случае она будет вертикальной, слева) и прикрепляем к последовательности полос.
QBarCategoryAxis *axisY = new QBarCategoryAxis();
axisY->setGridLineVisible(false);
axisY->setLineVisible(false);
axisY->append(categories);
chart->addAxis(axisY, Qt::AlignLeft);
series->attachAxis(axisY);
Создаем ось значений (объект QValueAxis
). Добавляем ее на диаграмму (она будет горизонтальной, внизу) и тоже прикрепляем к последовательности полос. После чего по определенному ранее максимальному значению, которое будет выведено на гистограмме, устанавливаем максимальное значение шкалы оси. Функция setAxisXMaxFromVal()
, выполняющая эту задачу, будет описана ниже.
QValueAxis *axisX = new QValueAxis();
chart->addAxis(axisX, Qt::AlignBottom);
series->attachAxis(axisX);
setAxisXMaxFromVal(maxValue, axisX);
Но нам необходимо, чтобы шкала оси значений QValueAxis
подстраивалась автоматически не только в момент запуска, но и во время работы приложения, поскольку скорости предполагаемых элементарных потоков могут внезапно достаточно сильно увеличиться. Для этого мы будем обрабатывать изменения значений каждой выводимой на гистограмме полосы, т.е. каждой полосы в каждом объекте QBarSet
.
В следующем фрагменте кода мы получаем все объекты QBarSet
в нашей последовательности (так как мы выводим на диаграмму только один столбец модели, то в нашем случае объект QBarSet
будет всего один) и к сигналу valueChanged(int index)
каждого из них привязываем слот processBarSetValueChange(int index)
, обрабатывающий изменения значений.
QList<QBarSet*> barsets = series->barSets();
for(int i = 0; i < barsets.size(); ++i)
{
connect(barsets.at(i), &QBarSet::valueChanged,
this, &MainWindow::processBarSetValueChange);
}
Чтобы избежать настройки QGraphicsScene
, мы используем класс QChartView
, который делает это за нас. Указатель на объект QChart
используется как параметр конструктора QChartView
. Чтобы визуализация выглядела лучше, включаем антиалиасинг и устанавливаем минимальный размер виджета chartView
.
chartView = new QChartView(chart);
chartView->setRenderHint(QPainter::Antialiasing);
chartView->setMinimumSize(640, 480);
И, наконец, помещаем виджет chartView
в специально подготовленный в дизайнере виджет QGroupBox
.
ui->chartGroupBox->layout()->addWidget(chartView);
Подстройка верхнего предела шкалы оси значений
Проверка необходимости изменения размера шкалы оси значений во время работы приложения выполняется в слоте processBarSetValueChange(int index)
, который ранее был привязан к сигналам valueChanged(int index)
объектов QBarSet
.
Здесь мы находим нашу ось значений QValueAxis
, сравниваем ее максимальное значение с полученным измененным значением объекта QBarSet
, выдавшего сигнал, и при необходимости изменяем верхний предел шкалы оси с помощью функции setAxisXMaxFromVal()
.
void MainWindow::processBarSetValueChange(int index)
{
QList<QAbstractAxis *> hAxises = chart->axes(Qt::Horizontal);
if(hAxises.isEmpty())
return;
QValueAxis *axisX = qobject_cast<QValueAxis *>(hAxises.at(0));
QBarSet *barset = qobject_cast<QBarSet*>(sender());
qreal value = barset->at(index);
if (value > axisX->max())
setAxisXMaxFromVal(value, axisX);
}
Изменение верхнего предела шкалы оси значений выполняется в функции setAxisXMaxFromVal()
. Для удобства пользователя в зависимости от максимального отображаемого значения выбираем верхний предел шкалы из ряда 5, 10, 50, 100, 500, 1000 и т.д. И устанавливаем определенное значение в качестве предельного значения шкалы оси значений QValueAxis
.
void MainWindow::setAxisXMaxFromVal(double val, QValueAxis *axis)
{
int degree = (int)floor(log10(abs(val)));
int scaled = val * pow(10, -degree);
int step2 = ((scaled +4)/5)*5;
int maxValue = step2 * pow(10, degree);
axis->setMax(maxValue);
}
В результате получаем приложение следующего вида. Первый скриншот – сразу после запуска, второй – через 5 секунд.
Исходный код
Полный исходный код проекта доступен на GitHub.
Также ниже приведен код исходного файлов mainwindow.cpp и mainwindow.h.
mainwindow.h
#ifndef MAINWINDOW_H
#define MAINWINDOW_H
#include <QMainWindow>
#include "bitratetablemodel.h"
#include <QtCharts>
#include <QSortFilterProxyModel>
QT_CHARTS_USE_NAMESPACE
QT_BEGIN_NAMESPACE
namespace Ui { class MainWindow; }
QT_END_NAMESPACE
class MainWindow : public QMainWindow
{
Q_OBJECT
public:
MainWindow(QWidget *parent = nullptr);
~MainWindow();
public slots:
void processBarSetValueChange(int index);
void setAxisXMaxFromVal(double val, QValueAxis *axis);
private:
Ui::MainWindow *ui;
BitrateTableModel *model;
QVBarModelMapper *mapper;
QChart *chart;
QChartView *chartView;
};
#endif // MAINWINDOW_H
mainwindow.cpp
#include "mainwindow.h"
#include "ui_mainwindow.h"
#include <QDebug>
//---------------------------------------------------------------------------------------
MainWindow::MainWindow(QWidget *parent)
: QMainWindow(parent)
, ui(new Ui::MainWindow)
{
ui->setupUi(this);
model = new BitrateTableModel;
ui->tableView->setModel(model);
ui->tableView->horizontalHeader()->setSectionResizeMode(QHeaderView::Stretch);
model->setParent(ui->tableView);
chart = new QChart;
QHorizontalBarSeries *series = new QHorizontalBarSeries;
mapper = new QVBarModelMapper(this);
mapper->setFirstBarSetColumn(BitrateTableModel::ColumnBitrate);
mapper->setLastBarSetColumn(BitrateTableModel::ColumnBitrate);
mapper->setSeries(series);
mapper->setModel(model);
chart->addSeries(series);
int rowCount = model->rowCount();
QStringList categories;
qreal maxValue = 0;
for(int i = 0; i < rowCount; ++i)
{
QModelIndex idx = model->index(i, BitrateTableModel::ColumnPid);
categories << model->data(idx).toString();
idx = model->index(i, BitrateTableModel::ColumnBitrate);
qreal curValue = model->data(idx).toDouble();
if (curValue > maxValue)
maxValue = curValue;
}
QBarCategoryAxis *axisY = new QBarCategoryAxis();
axisY->setGridLineVisible(false);
axisY->setLineVisible(false);
axisY->append(categories);
chart->addAxis(axisY, Qt::AlignLeft);
series->attachAxis(axisY);
QValueAxis *axisX = new QValueAxis();
chart->addAxis(axisX, Qt::AlignBottom);
series->attachAxis(axisX);
setAxisXMaxFromVal(maxValue, axisX);
QList<QBarSet*> barsets = series->barSets();
for(int i = 0; i < barsets.size(); ++i)
{
connect(barsets.at(i), &QBarSet::valueChanged,
this, &MainWindow::processBarSetValueChange);
}
chartView = new QChartView(chart);
chartView->setRenderHint(QPainter::Antialiasing);
chartView->setMinimumSize(640, 480);
ui->chartGroupBox->layout()->addWidget(chartView);
}
//---------------------------------------------------------------------------------------
MainWindow::~MainWindow()
{
delete ui;
}
//---------------------------------------------------------------------------------------
void MainWindow::processBarSetValueChange(int index)
{
QList<QAbstractAxis *> hAxises = chart->axes(Qt::Horizontal);
if(hAxises.isEmpty())
return;
QValueAxis *axisX = qobject_cast<QValueAxis *>(hAxises.at(0));
QBarSet *barset = qobject_cast<QBarSet*>(sender());
qreal value = barset->at(index);
if (value > axisX->max())
setAxisXMaxFromVal(value, axisX);
}
//---------------------------------------------------------------------------------------
void MainWindow::setAxisXMaxFromVal(double val, QValueAxis *axis)
{
int degree = (int)floor(log10(abs(val)));
int scaled = val * pow(10, -degree);
int step2 = ((scaled +4)/5)*5;
int maxValue = step2 * pow(10, degree);
axis->setMax(maxValue);
}
//---------------------------------------------------------------------------------------