Implemented statistics chart
This commit is contained in:
@@ -1,11 +1,14 @@
|
|||||||
QT = core gui widgets
|
QT = core gui widgets charts
|
||||||
|
|
||||||
CONFIG += c++14
|
CONFIG += c++14
|
||||||
|
|
||||||
DEFINES += QT_DEPRECATED_WARNINGS QT_DISABLE_DEPRECATED_BEFORE=0x060000
|
DEFINES += QT_DEPRECATED_WARNINGS QT_DISABLE_DEPRECATED_BEFORE=0x060000
|
||||||
|
|
||||||
SOURCES += main.cpp \
|
SOURCES += main.cpp \
|
||||||
|
chartperstory.cpp \
|
||||||
|
donutbreakdownchart.cpp \
|
||||||
flowlayout.cpp \
|
flowlayout.cpp \
|
||||||
|
mainslice.cpp \
|
||||||
mainwindow.cpp \
|
mainwindow.cpp \
|
||||||
stripsgrid.cpp \
|
stripsgrid.cpp \
|
||||||
stripwidget.cpp
|
stripwidget.cpp
|
||||||
@@ -15,7 +18,10 @@ FORMS += \
|
|||||||
stripwidget.ui
|
stripwidget.ui
|
||||||
|
|
||||||
HEADERS += \
|
HEADERS += \
|
||||||
|
chartperstory.h \
|
||||||
|
donutbreakdownchart.h \
|
||||||
flowlayout.h \
|
flowlayout.h \
|
||||||
|
mainslice.h \
|
||||||
mainwindow.h \
|
mainwindow.h \
|
||||||
stripsgrid.h \
|
stripsgrid.h \
|
||||||
stripwidget.h
|
stripwidget.h
|
||||||
|
36
chartperstory.cpp
Normal file
36
chartperstory.cpp
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
#include "chartperstory.h"
|
||||||
|
|
||||||
|
#include <QPieSeries>
|
||||||
|
#include <QGraphicsLayout>
|
||||||
|
|
||||||
|
#include "donutbreakdownchart.h"
|
||||||
|
|
||||||
|
QT_CHARTS_USE_NAMESPACE
|
||||||
|
|
||||||
|
ChartPerStory::ChartPerStory(QWidget *parent) :
|
||||||
|
QChartView{parent}
|
||||||
|
{
|
||||||
|
setRenderHint(QPainter::Antialiasing);
|
||||||
|
}
|
||||||
|
|
||||||
|
void ChartPerStory::setPoints(const QMap<QString, QMap<QString, int> > &points)
|
||||||
|
{
|
||||||
|
auto chart = new DonutBreakdownChart;
|
||||||
|
chart->layout()->setContentsMargins(0, 0, 0, 0);
|
||||||
|
chart->setBackgroundRoundness(0);
|
||||||
|
chart->setTitle("Story points with their subtasks");
|
||||||
|
chart->legend()->setAlignment(Qt::AlignBottom);
|
||||||
|
for (auto iter = std::begin(points); iter != std::end(points); iter++)
|
||||||
|
{
|
||||||
|
auto series = new QtCharts::QPieSeries();
|
||||||
|
series->setName(iter.key());
|
||||||
|
|
||||||
|
for (auto iter_ = std::begin(iter.value()); iter_ != std::end(iter.value()); iter_++)
|
||||||
|
series->append(iter_.key(), iter_.value());
|
||||||
|
|
||||||
|
chart->addBreakdownSeries(series, Qt::red);
|
||||||
|
}
|
||||||
|
setChart(chart);
|
||||||
|
delete m_chart;
|
||||||
|
m_chart = chart;
|
||||||
|
}
|
18
chartperstory.h
Normal file
18
chartperstory.h
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <QtCharts/QChartView>
|
||||||
|
|
||||||
|
class DonutBreakdownChart;
|
||||||
|
|
||||||
|
class ChartPerStory : public QtCharts::QChartView
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
|
||||||
|
public:
|
||||||
|
explicit ChartPerStory(QWidget *parent = nullptr);
|
||||||
|
|
||||||
|
void setPoints(const QMap<QString, QMap<QString, int>> &points);
|
||||||
|
|
||||||
|
private:
|
||||||
|
DonutBreakdownChart *m_chart{};
|
||||||
|
};
|
90
donutbreakdownchart.cpp
Normal file
90
donutbreakdownchart.cpp
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
#include "donutbreakdownchart.h"
|
||||||
|
|
||||||
|
#include <QtCharts/QPieSeries>
|
||||||
|
#include <QtCharts/QPieSlice>
|
||||||
|
#include <QtCharts/QPieLegendMarker>
|
||||||
|
|
||||||
|
#include "mainslice.h"
|
||||||
|
|
||||||
|
QT_CHARTS_USE_NAMESPACE
|
||||||
|
|
||||||
|
DonutBreakdownChart::DonutBreakdownChart(QGraphicsItem *parent, Qt::WindowFlags wFlags)
|
||||||
|
: QChart(QChart::ChartTypeCartesian, parent, wFlags)
|
||||||
|
{
|
||||||
|
// create the series for main center pie
|
||||||
|
m_mainSeries = new QPieSeries();
|
||||||
|
m_mainSeries->setPieSize(0.7);
|
||||||
|
QChart::addSeries(m_mainSeries);
|
||||||
|
}
|
||||||
|
|
||||||
|
void DonutBreakdownChart::addBreakdownSeries(QPieSeries *breakdownSeries, QColor color)
|
||||||
|
{
|
||||||
|
QFont font("Arial", 8);
|
||||||
|
|
||||||
|
// add breakdown series as a slice to center pie
|
||||||
|
MainSlice *mainSlice = new MainSlice(breakdownSeries);
|
||||||
|
mainSlice->setName(breakdownSeries->name());
|
||||||
|
mainSlice->setValue(breakdownSeries->sum());
|
||||||
|
m_mainSeries->append(mainSlice);
|
||||||
|
|
||||||
|
// customize the slice
|
||||||
|
mainSlice->setBrush(color);
|
||||||
|
mainSlice->setLabelVisible();
|
||||||
|
mainSlice->setLabelColor(Qt::white);
|
||||||
|
mainSlice->setLabelPosition(QPieSlice::LabelInsideHorizontal);
|
||||||
|
mainSlice->setLabelFont(font);
|
||||||
|
|
||||||
|
// position and customize the breakdown series
|
||||||
|
breakdownSeries->setPieSize(0.8);
|
||||||
|
breakdownSeries->setHoleSize(0.7);
|
||||||
|
breakdownSeries->setLabelsVisible();
|
||||||
|
const auto slices = breakdownSeries->slices();
|
||||||
|
for (QPieSlice *slice : slices) {
|
||||||
|
color = color.lighter(115);
|
||||||
|
slice->setBrush(color);
|
||||||
|
slice->setLabelFont(font);
|
||||||
|
}
|
||||||
|
|
||||||
|
// add the series to the chart
|
||||||
|
QChart::addSeries(breakdownSeries);
|
||||||
|
|
||||||
|
// recalculate breakdown donut segments
|
||||||
|
recalculateAngles();
|
||||||
|
|
||||||
|
// update customize legend markers
|
||||||
|
updateLegendMarkers();
|
||||||
|
}
|
||||||
|
|
||||||
|
void DonutBreakdownChart::recalculateAngles()
|
||||||
|
{
|
||||||
|
qreal angle = 0;
|
||||||
|
const auto slices = m_mainSeries->slices();
|
||||||
|
for (QPieSlice *slice : slices) {
|
||||||
|
QPieSeries *breakdownSeries = qobject_cast<MainSlice *>(slice)->breakdownSeries();
|
||||||
|
breakdownSeries->setPieStartAngle(angle);
|
||||||
|
angle += slice->percentage() * 360.0; // full pie is 360.0
|
||||||
|
breakdownSeries->setPieEndAngle(angle);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void DonutBreakdownChart::updateLegendMarkers()
|
||||||
|
{
|
||||||
|
// go through all markers
|
||||||
|
const auto allseries = series();
|
||||||
|
for (QAbstractSeries *series : allseries) {
|
||||||
|
const auto markers = legend()->markers(series);
|
||||||
|
for (QLegendMarker *marker : markers) {
|
||||||
|
QPieLegendMarker *pieMarker = qobject_cast<QPieLegendMarker *>(marker);
|
||||||
|
if (series == m_mainSeries) {
|
||||||
|
// hide markers from main series
|
||||||
|
pieMarker->setVisible(false);
|
||||||
|
} else {
|
||||||
|
// modify markers from breakdown series
|
||||||
|
pieMarker->setLabel(QString("%1 %2%")
|
||||||
|
.arg(pieMarker->slice()->label())
|
||||||
|
.arg(pieMarker->slice()->percentage() * 100, 0, 'f', 2));
|
||||||
|
pieMarker->setFont(QFont("Arial", 8));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
22
donutbreakdownchart.h
Normal file
22
donutbreakdownchart.h
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <QtCharts/QChart>
|
||||||
|
|
||||||
|
QT_CHARTS_BEGIN_NAMESPACE
|
||||||
|
class QPieSeries;
|
||||||
|
QT_CHARTS_END_NAMESPACE
|
||||||
|
|
||||||
|
class DonutBreakdownChart : public QtCharts::QChart
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
explicit DonutBreakdownChart(QGraphicsItem *parent = nullptr, Qt::WindowFlags wFlags = Qt::WindowFlags());
|
||||||
|
|
||||||
|
void addBreakdownSeries(QtCharts::QPieSeries *series, QColor color);
|
||||||
|
|
||||||
|
private:
|
||||||
|
void recalculateAngles();
|
||||||
|
void updateLegendMarkers();
|
||||||
|
|
||||||
|
private:
|
||||||
|
QtCharts::QPieSeries *m_mainSeries;
|
||||||
|
};
|
30
mainslice.cpp
Normal file
30
mainslice.cpp
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
#include "mainslice.h"
|
||||||
|
|
||||||
|
QT_CHARTS_USE_NAMESPACE
|
||||||
|
|
||||||
|
MainSlice::MainSlice(QPieSeries *breakdownSeries, QObject *parent)
|
||||||
|
: QPieSlice(parent),
|
||||||
|
m_breakdownSeries(breakdownSeries)
|
||||||
|
{
|
||||||
|
connect(this, &MainSlice::percentageChanged, this, &MainSlice::updateLabel);
|
||||||
|
}
|
||||||
|
|
||||||
|
QPieSeries *MainSlice::breakdownSeries() const
|
||||||
|
{
|
||||||
|
return m_breakdownSeries;
|
||||||
|
}
|
||||||
|
|
||||||
|
void MainSlice::setName(QString name)
|
||||||
|
{
|
||||||
|
m_name = name;
|
||||||
|
}
|
||||||
|
|
||||||
|
QString MainSlice::name() const
|
||||||
|
{
|
||||||
|
return m_name;
|
||||||
|
}
|
||||||
|
|
||||||
|
void MainSlice::updateLabel()
|
||||||
|
{
|
||||||
|
this->setLabel(QString("%1 %2%").arg(m_name).arg(percentage() * 100, 0, 'f', 2));
|
||||||
|
}
|
27
mainslice.h
Normal file
27
mainslice.h
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <QtCharts/QPieSlice>
|
||||||
|
|
||||||
|
QT_CHARTS_BEGIN_NAMESPACE
|
||||||
|
class QPieSeries;
|
||||||
|
QT_CHARTS_END_NAMESPACE
|
||||||
|
|
||||||
|
class MainSlice : public QtCharts::QPieSlice
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
|
||||||
|
public:
|
||||||
|
explicit MainSlice(QtCharts::QPieSeries *breakdownSeries, QObject *parent = 0);
|
||||||
|
|
||||||
|
QtCharts::QPieSeries *breakdownSeries() const;
|
||||||
|
|
||||||
|
void setName(QString name);
|
||||||
|
QString name() const;
|
||||||
|
|
||||||
|
public Q_SLOTS:
|
||||||
|
void updateLabel();
|
||||||
|
|
||||||
|
private:
|
||||||
|
QtCharts::QPieSeries *m_breakdownSeries;
|
||||||
|
QString m_name;
|
||||||
|
};
|
@@ -6,6 +6,15 @@ MainWindow::MainWindow(QWidget *parent) :
|
|||||||
m_ui{std::make_unique<Ui::MainWindow>()}
|
m_ui{std::make_unique<Ui::MainWindow>()}
|
||||||
{
|
{
|
||||||
m_ui->setupUi(this);
|
m_ui->setupUi(this);
|
||||||
|
|
||||||
|
connect(m_ui->stripsGrid, &StripsGrid::pointsPerStoryChanged, m_ui->chartPerStory, &ChartPerStory::setPoints);
|
||||||
|
m_ui->chartPerStory->setPoints(m_ui->stripsGrid->pointsPerStory());
|
||||||
|
|
||||||
|
for (QDockWidget *dockWidget : findChildren<QDockWidget*>())
|
||||||
|
{
|
||||||
|
m_ui->menu_View->addAction(dockWidget->toggleViewAction());
|
||||||
|
dockWidget->close();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
MainWindow::~MainWindow() = default;
|
MainWindow::~MainWindow() = default;
|
||||||
|
@@ -22,7 +22,7 @@
|
|||||||
<rect>
|
<rect>
|
||||||
<x>0</x>
|
<x>0</x>
|
||||||
<y>0</y>
|
<y>0</y>
|
||||||
<width>798</width>
|
<width>739</width>
|
||||||
<height>558</height>
|
<height>558</height>
|
||||||
</rect>
|
</rect>
|
||||||
</property>
|
</property>
|
||||||
@@ -43,9 +43,24 @@
|
|||||||
</property>
|
</property>
|
||||||
<addaction name="action_Quit"/>
|
<addaction name="action_Quit"/>
|
||||||
</widget>
|
</widget>
|
||||||
|
<widget class="QMenu" name="menu_View">
|
||||||
|
<property name="title">
|
||||||
|
<string>&View</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
<addaction name="menu_File"/>
|
<addaction name="menu_File"/>
|
||||||
|
<addaction name="menu_View"/>
|
||||||
</widget>
|
</widget>
|
||||||
<widget class="QStatusBar" name="statusbar"/>
|
<widget class="QStatusBar" name="statusbar"/>
|
||||||
|
<widget class="QDockWidget" name="dockWidget">
|
||||||
|
<property name="windowTitle">
|
||||||
|
<string>Points per story</string>
|
||||||
|
</property>
|
||||||
|
<attribute name="dockWidgetArea">
|
||||||
|
<number>1</number>
|
||||||
|
</attribute>
|
||||||
|
<widget class="ChartPerStory" name="chartPerStory"/>
|
||||||
|
</widget>
|
||||||
<action name="action_Quit">
|
<action name="action_Quit">
|
||||||
<property name="text">
|
<property name="text">
|
||||||
<string>&Quit</string>
|
<string>&Quit</string>
|
||||||
@@ -58,6 +73,12 @@
|
|||||||
<extends>QWidget</extends>
|
<extends>QWidget</extends>
|
||||||
<header>stripsgrid.h</header>
|
<header>stripsgrid.h</header>
|
||||||
</customwidget>
|
</customwidget>
|
||||||
|
<customwidget>
|
||||||
|
<class>ChartPerStory</class>
|
||||||
|
<extends>QWidget</extends>
|
||||||
|
<header>chartperstory.h</header>
|
||||||
|
<container>1</container>
|
||||||
|
</customwidget>
|
||||||
</customwidgets>
|
</customwidgets>
|
||||||
<resources/>
|
<resources/>
|
||||||
<connections>
|
<connections>
|
||||||
|
@@ -28,7 +28,7 @@ StripsGrid::StripsGrid(QWidget *parent) :
|
|||||||
|
|
||||||
QRandomGenerator random((qint32)(QDateTime::currentMSecsSinceEpoch()));
|
QRandomGenerator random((qint32)(QDateTime::currentMSecsSinceEpoch()));
|
||||||
|
|
||||||
const auto stories = random.bounded(3, 10);
|
const auto stories = random.bounded(3, 6);
|
||||||
|
|
||||||
int row{0};
|
int row{0};
|
||||||
{
|
{
|
||||||
@@ -86,7 +86,7 @@ StripsGrid::StripsGrid(QWidget *parent) :
|
|||||||
auto &test = story.columns[column-1];
|
auto &test = story.columns[column-1];
|
||||||
test.layout = new FlowLayout;
|
test.layout = new FlowLayout;
|
||||||
|
|
||||||
call_n_times(random.bounded(6), [&]()
|
call_n_times(random.bounded(4), [&]()
|
||||||
{
|
{
|
||||||
auto widget = new StripWidget{StripWidget::Subtask};
|
auto widget = new StripWidget{StripWidget::Subtask};
|
||||||
widget->setTitle(QString("ATC-%0").arg(random.bounded(1000, 5000)));
|
widget->setTitle(QString("ATC-%0").arg(random.bounded(1000, 5000)));
|
||||||
@@ -210,20 +210,26 @@ void StripsGrid::mouseMoveEvent(QMouseEvent *event)
|
|||||||
void StripsGrid::updatePoints()
|
void StripsGrid::updatePoints()
|
||||||
{
|
{
|
||||||
std::array<int, 5> sumsPerColumn{0,0,0,0,0};
|
std::array<int, 5> sumsPerColumn{0,0,0,0,0};
|
||||||
|
m_pointsPerStory.clear();
|
||||||
|
|
||||||
for (const auto &story : m_stories)
|
for (const auto &story : m_stories)
|
||||||
{
|
{
|
||||||
int storyPoints = 0;
|
int storyPoints = 0;
|
||||||
sumsPerColumn[0] += story.widget->points();
|
|
||||||
|
auto &test = m_pointsPerStory[story.widget->title()];
|
||||||
|
|
||||||
for (int i = 0; i < 4; i++)
|
for (int i = 0; i < 4; i++)
|
||||||
{
|
{
|
||||||
int columnSum = 0;
|
int columnSum = 0;
|
||||||
for (const auto *widget : story.columns[i].widgets)
|
for (const auto *widget : story.columns[i].widgets)
|
||||||
|
{
|
||||||
columnSum += widget->points();
|
columnSum += widget->points();
|
||||||
|
test[widget->title()] = widget->points();
|
||||||
|
}
|
||||||
sumsPerColumn[i+1] += columnSum;
|
sumsPerColumn[i+1] += columnSum;
|
||||||
storyPoints += columnSum;
|
storyPoints += columnSum;
|
||||||
}
|
}
|
||||||
|
sumsPerColumn[0] += storyPoints;
|
||||||
story.widget->setPoints(storyPoints);
|
story.widget->setPoints(storyPoints);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -237,4 +243,6 @@ void StripsGrid::updatePoints()
|
|||||||
|
|
||||||
for (int i = 0; i < 5; i++)
|
for (int i = 0; i < 5; i++)
|
||||||
m_tableHeader[i]->setText(texts[i].arg(sumsPerColumn[i]));
|
m_tableHeader[i]->setText(texts[i].arg(sumsPerColumn[i]));
|
||||||
|
|
||||||
|
emit pointsPerStoryChanged(m_pointsPerStory);
|
||||||
}
|
}
|
||||||
|
@@ -1,6 +1,7 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <QWidget>
|
#include <QWidget>
|
||||||
|
#include <QMap>
|
||||||
|
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
@@ -17,6 +18,11 @@ class StripsGrid : public QWidget
|
|||||||
public:
|
public:
|
||||||
explicit StripsGrid(QWidget *parent = nullptr);
|
explicit StripsGrid(QWidget *parent = nullptr);
|
||||||
|
|
||||||
|
const auto &pointsPerStory() const { return m_pointsPerStory; }
|
||||||
|
|
||||||
|
signals:
|
||||||
|
void pointsPerStoryChanged(const QMap<QString, QMap<QString, int> > &pointsPerStory);
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
void mousePressEvent(QMouseEvent *event) override;
|
void mousePressEvent(QMouseEvent *event) override;
|
||||||
void mouseReleaseEvent(QMouseEvent *event) override;
|
void mouseReleaseEvent(QMouseEvent *event) override;
|
||||||
@@ -46,4 +52,6 @@ private:
|
|||||||
};
|
};
|
||||||
|
|
||||||
std::vector<Story> m_stories;
|
std::vector<Story> m_stories;
|
||||||
|
|
||||||
|
QMap<QString, QMap<QString, int> > m_pointsPerStory;
|
||||||
};
|
};
|
||||||
|
Reference in New Issue
Block a user