diff --git a/.vscode/c_cpp_properties.json b/.vscode/c_cpp_properties.json index 439d492..22d6227 100644 --- a/.vscode/c_cpp_properties.json +++ b/.vscode/c_cpp_properties.json @@ -6,14 +6,14 @@ "${workspaceFolder}/**", "/usr/include/wx-3.0", "/usr/lib64/wx/include/gtk3-unicode-3.0", - "${workspaceFolder}/src/qt", - "/usr/include/libxml2", "/usr/include/libxml++-2.6", "/usr/include/glibmm-2.4", "/usr/include/glib-2.0/", "/usr/lib64/libxml++-2.6/include", "/usr/lib64/glibmm-2.4/include", "/usr/lib64/glib-2.0/include", + "${workspaceFolder}/src/qt", + "/usr/include/libxml2", "${workspaceFolder}/build/AssetFolio_autogen/include", "/usr/include/qt5", "/usr/include/qt5/QtWidgets", diff --git a/CMakeLists.txt b/CMakeLists.txt index e9b7dbd..4688ada 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -21,7 +21,9 @@ if("${GUI}" STREQUAL "Qt") set(GUI_SRCS src/qt/appgui.cpp src/qt/main.cpp + src/qt/callout.cpp src/qt/customtableview.cpp + src/qt/customgraphicview.cpp src/qt/appgui.ui) # Windows application icon diff --git a/data/example_isin.json b/data/example_isin.json index a3760ad..af3c70c 100644 --- a/data/example_isin.json +++ b/data/example_isin.json @@ -181,6 +181,15 @@ "Price": -8500, "Amount": -0.8, "Broker": "Brok" + }, + { + "Date": "23.06.2020", + "ID": "US037833AK68", + "Name": "Apple DL notes", + "AssetType": "ETF", + "Price": -300, + "Amount": -10, + "Broker": "ING" } ] } \ No newline at end of file diff --git a/docs/user_guide.md b/docs/user_guide.md index daadd3f..128fa86 100644 --- a/docs/user_guide.md +++ b/docs/user_guide.md @@ -21,7 +21,7 @@ If you never have a watchlist or a transaction list, you can just start a new em If have already a watchlist or an Excel list of your investments you can export it to a *JSON* file. The *JSON* file has to have three members: "QueryType", "Currency", and "Transactions". In order to create your own compatible json file, you can load the test json file [example.json](../data/example.json) for the symbol ticker or [example_isin.json](../data/example_isin.json) for ISIN. -Below is an example of a simple *JSON* file with a buying entry. +Below is an example of a simple *JSON* file with a buying entry. The Broker entry is optional and can be empty. ``` { diff --git a/src/AppControl.cpp b/src/AppControl.cpp index 4226b87..50fce50 100644 --- a/src/AppControl.cpp +++ b/src/AppControl.cpp @@ -18,7 +18,7 @@ #include #include #include // stringstream - +#include "../Config.h" using namespace std; const unsigned int MAX_WRITE_BUFFER = 65536; @@ -33,7 +33,7 @@ Provider::Provider(string name, string url, string xpath) AppControl::AppControl(unsigned int upd_freq) : _jsonDoc(make_shared()), _assets(make_shared>>()), _futures(), - _msg_queue(), _total_invested_values(0.), _total_current_values(0.),_isUpdateActive(false), _api_key(""), _update_freq(upd_freq), + _msg_queue(), _total_invested_values(0.), _total_current_values(0.), _isUpdateActive(false), _api_key(""), _update_freq(upd_freq), _currency_ref("USD"), _accumulated_roi() { // Add the HTML data provider @@ -113,7 +113,6 @@ bool AppControl::readLocalRapidJson(const char *filePath) _currency_ref = _jsonDoc->GetObject()["Currency"].GetString(); auto json_act = _jsonDoc->GetObject()["Transactions"].GetArray(); - double acc_roi = 0; int yy; int mm; @@ -123,6 +122,14 @@ bool AppControl::readLocalRapidJson(const char *filePath) time_t date; time(&rawtime); tm = *localtime(&rawtime); + if(json_act.Size()>0) + { + for (auto name = json_act[0].MemberBegin(); name < json_act[0].MemberEnd(); ++name) + { + Config::TRANSACTION_COL_NAMES.push_back(name->name.GetString()); + } + } + _total_invested_values=0; // creating all asset objects for (unsigned int i = 0; i < json_act.Size(); i++) { @@ -136,25 +143,23 @@ bool AppControl::readLocalRapidJson(const char *filePath) // Retrieve the asset information string name = json_act[i]["Name"].GetString(); string id = json_act[i]["ID"].GetString(); - Asset::Type asset_type = - Asset::_typeMap.at(json_act[i]["AssetType"].GetString()); - - - if (!json_act[i]["Amount"].IsNumber()) - { - throw AppException("JSON Amount of " + id + " has to be a number."); - } - float amount = json_act[i]["Amount"].GetFloat(); - if (!json_act[i]["Price"].IsNumber()) - { - throw AppException("JSON Transaction of " + id + - " has to be a number."); - } - float price = json_act[i]["Price"].GetFloat(); - // Check if the acquired id has already existed, if not then create - // a new asset try { + Asset::Type asset_type = + Asset::_typeMap.at(json_act[i]["AssetType"].GetString()); + if (!json_act[i]["Amount"].IsNumber()) + { + throw AppException("JSON Amount of " + id + " has to be a number."); + } + float amount = json_act[i]["Amount"].GetFloat(); + if (!json_act[i]["Price"].IsNumber()) + { + throw AppException("JSON Transaction of " + id + + " has to be a number."); + } + float price = json_act[i]["Price"].GetFloat(); + // Check if the acquired id has already existed, if not then create + // a new asset if (_assets->find(id) == _assets->end()) { // Differentiate the equity asset with the others if (asset_type == Asset::Type::Stock || @@ -178,16 +183,12 @@ bool AppControl::readLocalRapidJson(const char *filePath) _assets->find(id)->second->registerTransaction(date, amount, price); } _total_invested_values += price; - // collecting dividends - if ((amount==0) && (price>0)) - { - acc_roi += price; - _accumulated_roi.emplace(date, acc_roi); - } } catch (const std::exception &e) { - throw AppException(e.what()); + string msg = e.what(); + msg += ". Please check the data entry in " + strdate + " " + name + ".\nAsset Type shall be valid and not empty"; + throw AppException(msg); } } return true; @@ -617,7 +618,7 @@ const map &AppControl::getTotalRealizedRoi() map::iterator iteration = sorted_entries.find(iter->first); if (iteration == sorted_entries.end()) { - sorted_entries.insert(make_pair(iter->first, iter->second)); + sorted_entries.emplace(iter->first, iter->second); } else { @@ -627,9 +628,10 @@ const map &AppControl::getTotalRealizedRoi() } for (auto entry : sorted_entries) { - acc_val += entry.second; - _accumulated_roi.insert(make_pair(entry.first, acc_val)); + acc_val = acc_val + entry.second; + _accumulated_roi.emplace(entry.first, acc_val); } + return _accumulated_roi; } diff --git a/src/AppControl.h b/src/AppControl.h index 39d0e35..6684587 100644 --- a/src/AppControl.h +++ b/src/AppControl.h @@ -13,108 +13,106 @@ using namespace std; class Provider { - public: - string _name; - string _url; - string _xpath; - Provider(); - Provider(string name, string url, string xpath); +public: + string _name; + string _url; + string _xpath; + Provider(); + Provider(string name, string url, string xpath); }; class AppControl { - public: - AppControl(unsigned int upd_freq); - ~AppControl(); +public: + AppControl(unsigned int upd_freq); + ~AppControl(); - enum class QueryType - { - ISIN, - SYMBOL - }; + enum class QueryType + { + ISIN, + SYMBOL + }; - bool isApiKeyEmpty(); + bool isApiKeyEmpty(); - void setApiKey(string key); + void setApiKey(string key); - string getApiKey(); + string getApiKey(); - bool readApiKey(); + bool readApiKey(); - bool isEmpty(); + bool isEmpty(); - bool readLocalRapidJson(const char* filePath); + bool readLocalRapidJson(const char *filePath); - void writeDataToJson(vector& column_names); + void writeDataToJson(vector &column_names); - bool saveJson(string savepath); + bool saveJson(string savepath); - bool isAssetTypeValid(string input); + bool isAssetTypeValid(string input); - shared_ptr getJsonDoc() const; + shared_ptr getJsonDoc() const; - shared_ptr>> getAssets() const; + shared_ptr>> getAssets() const; - unique_ptr waitForUpdate(); + unique_ptr waitForUpdate(); - void calcAllocation(vector& categories, vector& values); - void calcCurrentAllocation(vector& categories, - vector& values); - void stopUpdateTasks(); + void calcAllocation(vector &categories, vector &values); + void calcCurrentAllocation(vector &categories, + vector &values); + void stopUpdateTasks(); - void launchAssetUpdater(); + void launchAssetUpdater(); - bool getPriceFromTradegate(vector>& updates); + bool getPriceFromTradegate(vector> &updates); - void clearJsonData(); + void clearJsonData(); - static string floatToString(float number, int precision); - static float stringToFloat(string numstr, int precision); - rapidjson::Value getQueryType(); - void setQueryType(string type); + static string floatToString(float number, int precision); + static float stringToFloat(string numstr, int precision); + rapidjson::Value getQueryType(); + void setQueryType(string type); - rapidjson::Value getCurrency(); + rapidjson::Value getCurrency(); - float getTotalInvestedValues() const; + float getTotalInvestedValues() const; - float getTotalCurrentValues() const; - - void setCurrency(string currency); + float getTotalCurrentValues() const; - struct AppException : public exception - { - string str; - AppException(string ss) : str(ss) {} - ~AppException() throw() {} // Updated - const char* what() const throw() { return str.c_str(); } - }; - - // year, realized RoI - const map>& getTotalRealizedRoi(); + void setCurrency(string currency); - private: - void calcCurrentTotalValues(); - void update(MsgQueue& msgqueue, bool& isActive, - unsigned int upd_frequency); - bool requestFmpApi(vector>& updates, string symbols); - float getExchangeRate(string from, string to); - void checkJson(); + struct AppException : public exception + { + string str; + AppException(string ss) : str(ss) {} + ~AppException() throw() {} // Updated + const char *what() const throw() { return str.c_str(); } + }; - shared_ptr _jsonDoc; - shared_ptr>> _assets; - vector> _futures; - MsgQueue _msg_queue; - float _total_invested_values; - float _total_current_values; + // year, realized RoI + const map> &getTotalRealizedRoi(); - bool _isUpdateActive; - string _api_key; - unsigned int _update_freq; - string _currency_ref; - map> _providers; - map _accumulated_roi; +private: + void calcCurrentTotalValues(); + void update(MsgQueue &msgqueue, bool &isActive, + unsigned int upd_frequency); + bool requestFmpApi(vector> &updates, string symbols); + float getExchangeRate(string from, string to); + void checkJson(); + shared_ptr _jsonDoc; + shared_ptr>> _assets; + vector> _futures; + MsgQueue _msg_queue; + float _total_invested_values; + float _total_current_values; + bool _isUpdateActive; + string _api_key; + unsigned int _update_freq; + string _currency_ref; + map> _providers; + map _accumulated_roi; }; #endif diff --git a/src/Asset.cpp b/src/Asset.cpp index c43ca83..39def1f 100644 --- a/src/Asset.cpp +++ b/src/Asset.cpp @@ -19,11 +19,10 @@ const map Asset::_typeMap = { {"Commodity", Type::Commodity}, {"Others", Type::Others}}; - Asset::Asset(string id, string name, Type type) : _id(id), _name(name), _type(type), _amount(0), _balance(0), _avg_price(0), _curr_price(0), _curr_value(0), _diff(0), _diff_in_percent(0), _return(0), - _return_in_percent(0), _profit_loss(0), _return_years(), _rois() + _return_in_percent(0), _profit_loss(0), _profit_in_percent(0), _last_accumulated(0), _return_years(), _rois() { } @@ -31,15 +30,15 @@ Asset::~Asset() {} void Asset::registerTransaction(time_t reg_date, float amount, float value_incl_fees) { - if ((amount>0) && (value_incl_fees>0)) - {// Buy transaction + if ((amount > 0) && (value_incl_fees > 0)) + { // Buy transaction _amount = _amount + amount; _balance = _balance + value_incl_fees; _avg_price = _balance / _amount; updateYearlyReturn(reg_date, _balance, 0); } - else if ((amount<0) && (value_incl_fees<0)) - {// Sell transaction + else if ((amount < 0) && (value_incl_fees < 0)) + { // Sell transaction float selling_price = -1 * value_incl_fees; float selling_amount = -1 * amount; if (_amount < selling_amount) @@ -47,7 +46,7 @@ void Asset::registerTransaction(time_t reg_date, float amount, float value_incl_ // calc the profit based on the previous average buying price _profit_loss = selling_price - (selling_amount * _avg_price); _amount = _amount - selling_amount; - + if (_amount == 0) { _avg_price = 0; @@ -64,8 +63,8 @@ void Asset::registerTransaction(time_t reg_date, float amount, float value_incl_ updateYearlyRoi(reg_date, _profit_loss); } - else if ((amount==0) && (value_incl_fees>0)) - {// Realized profit or dividend + else if ((amount == 0) && (value_incl_fees > 0)) + { // Realized profit or dividend _return = _return + value_incl_fees; _return_in_percent = _return / _balance * 100; updateYearlyReturn(reg_date, _balance, value_incl_fees); @@ -73,7 +72,7 @@ void Asset::registerTransaction(time_t reg_date, float amount, float value_incl_ } else { - cout << "Error: Unknown transaction" << endl; + throw std::runtime_error("Invalid entry: Unknown transaction in " + _name + " " + _id); } } @@ -110,8 +109,7 @@ void Asset::updateYearlyReturn(time_t reg_date, float total_value, { // create a new entry of the year YearlyReturn year_returns(register_year, total_value, returns, returns / total_value * 100.); - _return_years.insert( - pair(register_year, year_returns)); + _return_years.emplace(register_year, year_returns); } } @@ -120,6 +118,7 @@ void Asset::updateYearlyRoi(time_t reg_date, float value) struct tm *tmp = gmtime(®_date); string date = to_string(tmp->tm_mday) + "." + to_string(tmp->tm_mon + 1) + "." + to_string(tmp->tm_year + 1900); map::iterator find_it = _rois.find(reg_date); + _last_accumulated +=value; if (find_it != _rois.end()) { find_it->second += value; @@ -127,7 +126,7 @@ void Asset::updateYearlyRoi(time_t reg_date, float value) else { time_t newtime = reg_date; - _rois.insert(make_pair(newtime, value)); + _rois.emplace(newtime, _last_accumulated); } } diff --git a/src/Asset.h b/src/Asset.h index 216e2d5..f3d9b51 100644 --- a/src/Asset.h +++ b/src/Asset.h @@ -15,100 +15,99 @@ using namespace std; struct YearlyReturn { - int _year; - float _total_value; - float _total_return; - // return in percent - float _return_in_percent; - YearlyReturn(int year, float val, float ret, float ret_percent) - : _year(year), _total_value(val), _total_return(ret), - _return_in_percent(ret_percent){}; + int _year; + float _total_value; + float _total_return; + // return in percent + float _return_in_percent; + YearlyReturn(int year, float val, float ret, float ret_percent) + : _year(year), _total_value(val), _total_return(ret), + _return_in_percent(ret_percent){}; }; class Asset : public std::enable_shared_from_this { - public: - - enum class Type - { - Stock, - ETF, - Bond, - Real_Estate, - Crypto, - Commodity, - Others - }; - static const map _typeMap; - // constructor & destructor - Asset(string id, string name, Type type); - ~Asset(); - - void registerTransaction(time_t reg_date, float amount, float price_incl_fees); - - void updateYearlyReturn(time_t reg_date, float total_value, - float returns = 0.); - - // get the asset id - string getId() const; - // get the asset name - string getName() const; - // Get the asset's amount - float getAmount() const; - float getBalance() const; - float getAvgPrice() const; - float getCurrPrice() const; - float getCurrValue() const; - float getDiff() const; - float getDiffInPercent() const; - float getReturn() const; - float getReturnInPercent() const; - float getProfitLoss() const; - // Get the asset type - Type getType() const; - - void setCurrPrice(float price); - - void updateYearlyRoi(time_t reg_date, float value); - // get the realized RoI - const map& getRois(); - - protected: - std::shared_ptr get_shared_this() { return shared_from_this(); } - - /* data */ - string _id; - string _name; - // asset type - Type _type; - // total amount of the asset - float _amount; - // total spending - float _balance; - // average buying price - float _avg_price; - // current price - float _curr_price; - // current total value - float _curr_value; - - // Difference to avg buying price - float _diff; - // Difference to avg buying price in percent - float _diff_in_percent; - - // asset return in percent - float _return; - float _return_in_percent; - - float _profit_loss; - float _profit_in_percent; - - // list of the returns with its value and its returns - map _return_years; - // date time, roi value - map _rois; - static mutex _mtx; +public: + enum class Type + { + Stock, + ETF, + Bond, + Real_Estate, + Crypto, + Commodity, + Others + }; + static const map _typeMap; + // constructor & destructor + Asset(string id, string name, Type type); + ~Asset(); + + void registerTransaction(time_t reg_date, float amount, float price_incl_fees); + + void updateYearlyReturn(time_t reg_date, float total_value, + float returns = 0.); + + // get the asset id + string getId() const; + // get the asset name + string getName() const; + // Get the asset's amount + float getAmount() const; + float getBalance() const; + float getAvgPrice() const; + float getCurrPrice() const; + float getCurrValue() const; + float getDiff() const; + float getDiffInPercent() const; + float getReturn() const; + float getReturnInPercent() const; + float getProfitLoss() const; + // Get the asset type + Type getType() const; + + void setCurrPrice(float price); + + void updateYearlyRoi(time_t reg_date, float value); + // get the realized RoI + const map &getRois(); + +protected: + std::shared_ptr get_shared_this() { return shared_from_this(); } + + /* data */ + string _id; + string _name; + // asset type + Type _type; + // total amount of the asset + float _amount; + // total spending + float _balance; + // average buying price + float _avg_price; + // current price + float _curr_price; + // current total value + float _curr_value; + + // Difference to avg buying price + float _diff; + // Difference to avg buying price in percent + float _diff_in_percent; + + // asset return in percent + float _return; + float _return_in_percent; + + float _profit_loss; + float _profit_in_percent; + float _last_accumulated; + // list of the returns with its value and its returns + map _return_years; + // date time, roi value + map _rois; + static mutex _mtx; }; #endif \ No newline at end of file diff --git a/src/Config.cpp b/src/Config.cpp index f83cbdd..a2ff09f 100644 --- a/src/Config.cpp +++ b/src/Config.cpp @@ -2,9 +2,10 @@ const unsigned int Config::UPDATE_PERIODE = 20; -std::vector Config::TRANSACTION_COL_NAMES = { - "Date", "ID", "Name", "AssetType", "Price", "Amount", "Broker"}; +std::vector Config::TRANSACTION_COL_NAMES = {}; std::vector Config::WATCHLIST_COL_NAMES{ "ID", "Name", "Amount", "Balance", "Avg Price", "Curr.Price", - "Curr.Value", "Diff", "Diff %", "Return", "Return %", "Profit"}; \ No newline at end of file + "Curr.Value", "Diff", "Diff %", "Return", "Return %", "Profit"}; + +std::string Config::DATE_FORMAT= "dd.MM.yyyy"; \ No newline at end of file diff --git a/src/Config.h b/src/Config.h index d7e2cce..aa6c7f6 100644 --- a/src/Config.h +++ b/src/Config.h @@ -11,6 +11,7 @@ class Config static const unsigned int UPDATE_PERIODE; static std::vector TRANSACTION_COL_NAMES; static std::vector WATCHLIST_COL_NAMES; + static std::string DATE_FORMAT; }; #endif \ No newline at end of file diff --git a/src/qt/appgui.cpp b/src/qt/appgui.cpp index e30676e..6034d16 100644 --- a/src/qt/appgui.cpp +++ b/src/qt/appgui.cpp @@ -13,10 +13,7 @@ AppGui::AppGui(QWidget *parent) _qchart(new QtCharts::QChart()), _pieseries(new QtCharts::QPieSeries()), _chartView(new QtCharts::QChartView(_qchart)), - _roichart(new QtCharts::QChart()), - _roiChartView(new QtCharts::QChartView(_roichart)), - _axisX(new QtCharts::QDateTimeAxis()), - _axisY(new QtCharts::QValueAxis()), + _roiChartView(new CustomGraphicView("Realized RoI", QString(Config::DATE_FORMAT.c_str()))), _roi_date_series(new QtCharts::QLineSeries()), _appControl(make_shared(Config::UPDATE_PERIODE)), _transaction_model(make_shared( @@ -28,15 +25,12 @@ AppGui::AppGui(QWidget *parent) _qchart->setAnimationOptions(QtCharts::QChart::AllAnimations); _qchart->legend()->setAlignment(Qt::AlignRight); _chartView->setRenderHint(QPainter::Antialiasing); - _chartView->chart()->setTheme(QtCharts::QChart::ChartThemeBlueCerulean); + _chartView->chart()->setTheme(QtCharts::QChart::ChartThemeDark); + _chartView->setInteractive(true); layout.addWidget(_chartView); // set the chart to the tab widget ui->tab_alloc->setLayout(&layout); - // init ROI plot - - _roiChartView->chart()->addAxis(_axisX, Qt::AlignBottom); - _roiChartView->chart()->addAxis(_axisY, Qt::AlignLeft); layout_roi.addWidget(_roiChartView); ui->tab_plots->setLayout(&layout_roi); } @@ -69,7 +63,6 @@ void AppGui::on_actionOpen_triggered() shared_ptr jsonDoc = _appControl->getJsonDoc(); auto json_entries = jsonDoc->GetObject()["Transactions"].GetArray(); - int rowPos = 0; int colPos = 0; int offset = 10; @@ -147,7 +140,7 @@ void AppGui::on_actionOpen_triggered() // Set the DateDelegate for column sorting by date ui->tableView->setItemDelegateForColumn(0, new DateDelegate); - string status_bar = "Total invested value is " +AppControl::floatToString( _appControl->getTotalInvestedValues(),2) + " "+ _appControl->getCurrency().GetString(); + string status_bar = "Total invested value is " + AppControl::floatToString(_appControl->getTotalInvestedValues(), 2) + " " + _appControl->getCurrency().GetString(); statusBar()->showMessage(tr(status_bar.c_str())); // Fill the watchlist viewer vector colWatchlist = { @@ -156,9 +149,11 @@ void AppGui::on_actionOpen_triggered() "Change", "Yield %", "TotalYield"}; // update the piechart + vector data; vector categories; _appControl->calcAllocation(categories, data); + createPieChart(categories, data); createRoiChart(); } @@ -184,7 +179,7 @@ void AppGui::on_actionExit_triggered() void AppGui::on_actionInfo_triggered() { showMsgWindow( - QMessageBox::Information, "About Assetfolio 1.0", + QMessageBox::Information, "About Assetfolio 1.5", "An Asset Portfolio Tracker Application that keeps your asset data " "private. We don't need to signup and give up our data to the cloud " "server.\nCheck and read the README in " @@ -347,7 +342,7 @@ void AppGui::on_tbtnTransaction_clicked() ui->tableView->setModel(_transaction_model.get()); ui->tableView->resizeColumnsToContents(); _appControl->stopUpdateTasks(); - string status_bar = "Total invested value is " +AppControl::floatToString( _appControl->getTotalInvestedValues(),2) + " "+ _appControl->getCurrency().GetString(); + string status_bar = "Total invested value is " + AppControl::floatToString(_appControl->getTotalInvestedValues(), 2) + " " + _appControl->getCurrency().GetString(); statusBar()->showMessage(tr(status_bar.c_str())); } @@ -431,7 +426,7 @@ void AppGui::updateWatchlistModel(UpdateData upd_data) vector data; vector categories; _appControl->calcCurrentAllocation(categories, data); - string status_bar = "Total current asset value is " +AppControl::floatToString( _appControl->getTotalCurrentValues(),2) + " "+ _appControl->getCurrency().GetString(); + string status_bar = "Total current asset value is " + AppControl::floatToString(_appControl->getTotalCurrentValues(), 2) + " " + _appControl->getCurrency().GetString(); statusBar()->showMessage(tr(status_bar.c_str())); } @@ -552,7 +547,6 @@ void AppGui::createPieChart(vector &categories, vector &data) { _pieseries->append(categories[i].c_str(), data[i]); } - if (_qchart->series().size() == 0) { _pieseries->setHoleSize(0.35); @@ -598,7 +592,8 @@ void AppGui::createRoiChart() maxdatetime.setDate(roidate); } numTick++; - orderedROI.insert(pair(roidate, it->second)); + + orderedROI.emplace(roidate, it->second); values.push_back(it->second); } for (auto iter = orderedROI.begin(); iter != orderedROI.end(); ++iter) @@ -611,25 +606,27 @@ void AppGui::createRoiChart() // Attach axes after adding the data series to the chart if (_roi_date_series->attachedAxes().size() == 0) { - _roiChartView->chart()->addSeries(_roi_date_series); - _roiChartView->chart()->setTitle("Realized RoI"); - _roi_date_series->attachAxis(_axisX); - _axisX->setTickCount(numTick * 2); - _axisX->setFormat("MMM yyyy"); - _axisX->setTitleText("Date"); - _roi_date_series->attachAxis(_axisY); - _axisY->setLabelFormat("%i"); - } + if (numTick < 5) + numTick = numTick * 2; + else if (numTick > 12) + numTick = 12; + _roiChartView->connectDataSeries(_roi_date_series, numTick); + } // Update the axis ranges - _axisX->setRange(mindatetime, maxdatetime); - std::vector::iterator minIterator = min_element(values.begin(), values.end()); - std::vector::iterator maxIterator = max_element(values.begin(), values.end()); - _axisY->setRange(*minIterator, *maxIterator); + _roiChartView->axisX()->setRange(mindatetime, maxdatetime); + if (values.size() > 1) + { + std::vector::iterator minIterator = min_element(values.begin(), values.end()); + std::vector::iterator maxIterator = max_element(values.begin(), values.end()); + _roiChartView->axisY()->setRange(*minIterator, *maxIterator); + } + else + _roiChartView->axisY()->setRange(0, 1); + QString title = "RoI in "; title += _appControl->getCurrency().GetString(); - _axisY->setTitleText(title); - + _roiChartView->axisY()->setTitleText(title); _roiChartView->setRenderHint(QPainter::Antialiasing); } diff --git a/src/qt/appgui.h b/src/qt/appgui.h index 55f35c5..b4a43d4 100644 --- a/src/qt/appgui.h +++ b/src/qt/appgui.h @@ -19,11 +19,12 @@ #include #include "../AppControl.h" #include "../MsgQueue.h" +#include "customgraphicview.h" QT_BEGIN_NAMESPACE namespace Ui { -class AppGui; + class AppGui; } QT_END_NAMESPACE @@ -35,16 +36,16 @@ class UpdaterThread : public QThread QMutex mutex; QWaitCondition condition; - signals: +signals: void updatedAsset(UpdateData upd_data); - protected: +protected: void run() override; - public: +public: UpdaterThread() { _is_start = true; }; explicit UpdaterThread(shared_ptr appCtrl, - QObject* parent = nullptr) + QObject *parent = nullptr) : QThread(parent) { // _app_control = appCtrl; @@ -69,11 +70,11 @@ class AppGui : public QMainWindow { Q_OBJECT - public: - AppGui(QWidget* parent = nullptr); +public: + AppGui(QWidget *parent = nullptr); ~AppGui(); - private slots: +private slots: void on_actionNew_triggered(); void on_actionExit_triggered(); @@ -92,16 +93,15 @@ class AppGui : public QMainWindow void on_tbtnWatchlist_clicked(); - private: - Ui::AppGui* ui; - QtCharts::QChart* _qchart; - QtCharts::QPieSeries* _pieseries; - QtCharts::QChartView* _chartView; - QtCharts::QChart* _roichart; - QtCharts::QChartView* _roiChartView; - QtCharts::QDateTimeAxis* _axisX; - QtCharts::QValueAxis* _axisY; - QtCharts::QLineSeries* _roi_date_series; +private: + Ui::AppGui *ui; + QtCharts::QChart *_qchart; + QtCharts::QPieSeries *_pieseries; + QtCharts::QChartView *_chartView; + CustomGraphicView *_roiChartView; + // QtCharts::QDateTimeAxis *_axisX; + // QtCharts::QValueAxis *_axisY; + QtCharts::QLineSeries *_roi_date_series; QGridLayout layout; QGridLayout layout_roi; @@ -115,12 +115,12 @@ class AppGui : public QMainWindow // internal function void initTvTransactions(unsigned int row, unsigned int col); void initWatchlistModel(); - void showMsgWindow(QMessageBox::Icon&& msgtype, + void showMsgWindow(QMessageBox::Icon &&msgtype, const std::string title, const std::string msg); - void closeEvent(QCloseEvent* event); + void closeEvent(QCloseEvent *event); - void createPieChart(vector& categories, vector& data); + void createPieChart(vector &categories, vector &data); void createRoiChart(); void watchlistUpdater(); void setWatchlistColor(float update_var, @@ -128,4 +128,4 @@ class AppGui : public QMainWindow uint colidx, float threshold = 10.); }; -#endif // APPGUI_H +#endif // APPGUI_H diff --git a/src/qt/callout.cpp b/src/qt/callout.cpp new file mode 100644 index 0000000..3313d26 --- /dev/null +++ b/src/qt/callout.cpp @@ -0,0 +1,113 @@ +#include "callout.h" +#include +#include +#include +#include +#include + +Callout::Callout(QChart *chart) : QGraphicsItem(chart), + m_chart(chart) +{ +} + +QRectF Callout::boundingRect() const +{ + QPointF anchor = mapFromParent(m_chart->mapToPosition(m_anchor)); + QRectF rect; + rect.setLeft(qMin(m_rect.left(), anchor.x())); + rect.setRight(qMax(m_rect.right(), anchor.x())); + rect.setTop(qMin(m_rect.top(), anchor.y())); + rect.setBottom(qMax(m_rect.bottom(), anchor.y())); + return rect; +} + +void Callout::paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget) +{ + Q_UNUSED(option) + Q_UNUSED(widget) + QPainterPath path; + path.addRoundedRect(m_rect, 5, 5); + + QPointF anchor = mapFromParent(m_chart->mapToPosition(m_anchor)); + if (!m_rect.contains(anchor)) + { + QPointF point1, point2; + + // establish the position of the anchor point in relation to m_rect + bool above = anchor.y() <= m_rect.top(); + bool aboveCenter = anchor.y() > m_rect.top() && anchor.y() <= m_rect.center().y(); + bool belowCenter = anchor.y() > m_rect.center().y() && anchor.y() <= m_rect.bottom(); + bool below = anchor.y() > m_rect.bottom(); + + bool onLeft = anchor.x() <= m_rect.left(); + bool leftOfCenter = anchor.x() > m_rect.left() && anchor.x() <= m_rect.center().x(); + bool rightOfCenter = anchor.x() > m_rect.center().x() && anchor.x() <= m_rect.right(); + bool onRight = anchor.x() > m_rect.right(); + + // get the nearest m_rect corner. + qreal x = (onRight + rightOfCenter) * m_rect.width(); + qreal y = (below + belowCenter) * m_rect.height(); + bool cornerCase = (above && onLeft) || (above && onRight) || (below && onLeft) || (below && onRight); + bool vertical = qAbs(anchor.x() - x) > qAbs(anchor.y() - y); + + qreal x1 = x + leftOfCenter * 10 - rightOfCenter * 20 + cornerCase * !vertical * (onLeft * 10 - onRight * 20); + qreal y1 = y + aboveCenter * 10 - belowCenter * 20 + cornerCase * vertical * (above * 10 - below * 20); + ; + point1.setX(x1); + point1.setY(y1); + + qreal x2 = x + leftOfCenter * 20 - rightOfCenter * 10 + cornerCase * !vertical * (onLeft * 20 - onRight * 10); + ; + qreal y2 = y + aboveCenter * 20 - belowCenter * 10 + cornerCase * vertical * (above * 20 - below * 10); + ; + point2.setX(x2); + point2.setY(y2); + + path.moveTo(point1); + path.lineTo(anchor); + path.lineTo(point2); + path = path.simplified(); + } + painter->setBrush(QColor(255, 255, 255)); + painter->drawPath(path); + painter->drawText(m_textRect, m_text); +} + +void Callout::mousePressEvent(QGraphicsSceneMouseEvent *event) +{ + event->setAccepted(true); +} + +void Callout::mouseMoveEvent(QGraphicsSceneMouseEvent *event) +{ + if (event->buttons() & Qt::LeftButton) + { + setPos(mapToParent(event->pos() - event->buttonDownPos(Qt::LeftButton))); + event->setAccepted(true); + } + else + { + event->setAccepted(false); + } +} + +void Callout::setText(const QString &text) +{ + m_text = text; + QFontMetrics metrics(m_font); + m_textRect = metrics.boundingRect(QRect(0, 0, 150, 150), Qt::AlignLeft, m_text); + m_textRect.translate(5, 5); + prepareGeometryChange(); + m_rect = m_textRect.adjusted(-5, -5, 5, 5); +} + +void Callout::setAnchor(QPointF point) +{ + m_anchor = point; +} + +void Callout::updateGeometry() +{ + prepareGeometryChange(); + setPos(m_chart->mapToPosition(m_anchor) + QPoint(10, -50)); +} \ No newline at end of file diff --git a/src/qt/callout.h b/src/qt/callout.h new file mode 100644 index 0000000..ea8e94d --- /dev/null +++ b/src/qt/callout.h @@ -0,0 +1,43 @@ +#ifndef CALLOUT_H +#define CALLOUT_H + +#include +#include +#include + +QT_BEGIN_NAMESPACE +class QGraphicsSceneMouseEvent; +QT_END_NAMESPACE + +QT_CHARTS_BEGIN_NAMESPACE +class QChart; +QT_CHARTS_END_NAMESPACE + +QT_CHARTS_USE_NAMESPACE + +class Callout : public QGraphicsItem +{ +public: + Callout(QChart *parent); + + void setText(const QString &text); + void setAnchor(QPointF point); + void updateGeometry(); + + QRectF boundingRect() const; + void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget); + +protected: + void mousePressEvent(QGraphicsSceneMouseEvent *event); + void mouseMoveEvent(QGraphicsSceneMouseEvent *event); + +private: + QString m_text; + QRectF m_textRect; + QRectF m_rect; + QPointF m_anchor; + QFont m_font; + QChart *m_chart; +}; + +#endif // CALLOUT_H diff --git a/src/qt/customgraphicview.cpp b/src/qt/customgraphicview.cpp new file mode 100644 index 0000000..23058ef --- /dev/null +++ b/src/qt/customgraphicview.cpp @@ -0,0 +1,157 @@ +#include "customgraphicview.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "callout.h" +#include "../Config.h" + +CustomGraphicView::CustomGraphicView(const QString &title, const QString &dateformat, QWidget *parent) + : QGraphicsView(new QGraphicsScene, parent), + _coordX(0), + _coordY(0), + _axisX(new QtCharts::QDateTimeAxis()), + _axisY(new QtCharts::QValueAxis()), + _chart(0), + _tooltip(0), + _hoveredDate(""), + _dateformat(dateformat), + _numScheduledScalings(0) +{ + // chart + _chart = new QChart(); + _chart->setTitle(title); + _chart->setTheme(QtCharts::QChart::ChartThemeDark); + _chart->addAxis(_axisX, Qt::AlignBottom); + _chart->addAxis(_axisY, Qt::AlignLeft); + _chart->setAcceptHoverEvents(true); + + setInteractive(true); + setRenderHint(QPainter::Antialiasing); + setMouseTracking(true); + scene()->addItem(_chart); + + _coordX = new QGraphicsSimpleTextItem(_chart); + _coordX->setPos(_chart->size().width() / 2 - 100, _chart->size().height()); + + _coordY = new QGraphicsSimpleTextItem(_chart); + _coordY->setPos(_chart->size().width() / 2 + 50, _chart->size().height()); + + +} + +QtCharts::QDateTimeAxis *CustomGraphicView::axisX() +{ + return _axisX; +} + +QtCharts::QValueAxis *CustomGraphicView::axisY() +{ + return _axisY; +} + +void CustomGraphicView::connectDataSeries(QLineSeries *series, int numTick) +{ + _chart->addSeries(series); + _axisX->setTickCount(numTick); + _axisX->setFormat("MMM.yy"); + _axisX->setTitleText("Date"); + series->attachAxis(_axisX); + series->attachAxis(_axisY); + _axisY->setLabelFormat("%i"); + setRenderHint(QPainter::Antialiasing); + series->connect(series, &QLineSeries::clicked, this, &CustomGraphicView::keepCallout); + connect(series, &QLineSeries::hovered, this, &CustomGraphicView::tooltip); +} + +// Callout Implementation +void CustomGraphicView::resizeEvent(QResizeEvent *event) +{ + if (scene()) + { + scene()->setSceneRect(QRect(QPoint(0, 0), event->size())); + _chart->resize(event->size()); + _coordX->setPos(_chart->size().width() / 2 - 50, _chart->size().height() - 20); + _coordY->setPos(_chart->size().width() / 2 + 50, _chart->size().height() - 20); + const auto callouts = _callouts; + for (Callout *callout : callouts) + callout->updateGeometry(); + } + QGraphicsView::resizeEvent(event); +} + +void CustomGraphicView::mouseMoveEvent(QMouseEvent *event) +{ + _hoveredDate = QDateTime::fromMSecsSinceEpoch(_chart->mapToValue(event->pos()).x()).toString(_dateformat); + // _coordX->setText(QString("Date: %1").arg(_hoveredDate)); + // _coordY->setText(QString("Value: %1").arg(_chart->mapToValue(event->pos()).y())); + QGraphicsView::mouseMoveEvent(event); +} + +void CustomGraphicView::keepCallout() +{ + _callouts.append(_tooltip); + _tooltip = new Callout(_chart); +} + +void CustomGraphicView::tooltip(QPointF point, bool state) +{ + if (_tooltip == 0) + _tooltip = new Callout(_chart); + + if (state) + { + _tooltip->setText(QString("Date: %1 \nValue: %2 ").arg(_hoveredDate).arg(point.y())); + _tooltip->setAnchor(point); + _tooltip->setZValue(11); + _tooltip->updateGeometry(); + _tooltip->show(); + } + else + { + _tooltip->hide(); + } +} + +// Zoom In/Out +// ------------ +void CustomGraphicView::keyPressEvent(QKeyEvent *event) +{ + switch (event->key()) + { + case Qt::Key_Plus: + _chart->zoomIn(); + break; + case Qt::Key_Minus: + _chart->zoomOut(); + break; + case Qt::Key_Left: + _chart->scroll(-10, 0); + break; + case Qt::Key_Right: + _chart->scroll(10, 0); + break; + case Qt::Key_Up: + _chart->scroll(0, 10); + break; + case Qt::Key_Down: + _chart->scroll(0, -10); + break; + default: + QGraphicsView::keyPressEvent(event); + break; + } +} + +void CustomGraphicView::wheelEvent(QWheelEvent *event) +{ + event->delta() > 0 ? _chart->zoomIn() : _chart->zoomOut(); +} diff --git a/src/qt/customgraphicview.h b/src/qt/customgraphicview.h new file mode 100644 index 0000000..c3c2e1b --- /dev/null +++ b/src/qt/customgraphicview.h @@ -0,0 +1,55 @@ +#ifndef CUSTOMGRAPHICVIEW_H +#define CUSTOMGRAPHICVIEW_H +#include +#include +#include +#include +#include +#include + +QT_BEGIN_NAMESPACE +class QGraphicsScene; +class QMouseEvent; +class QResizeEvent; +QT_END_NAMESPACE + +class Callout; + +QT_CHARTS_USE_NAMESPACE + +class CustomGraphicView : public QGraphicsView +{ + Q_OBJECT + +public: + CustomGraphicView(const QString &title, const QString &dateformat, QWidget *parent = 0); + void connectDataSeries(QLineSeries *series, int numTick); + QtCharts::QDateTimeAxis *axisX(); + QtCharts::QValueAxis *axisY(); + +protected: + void resizeEvent(QResizeEvent *event); + void mouseMoveEvent(QMouseEvent *event); + + void keyPressEvent(QKeyEvent *event); + void wheelEvent(QWheelEvent *event); + + +public slots: + void keepCallout(); + void tooltip(QPointF point, bool state); + +private: + QGraphicsSimpleTextItem *_coordX; + QGraphicsSimpleTextItem *_coordY; + QtCharts::QDateTimeAxis *_axisX; + QtCharts::QValueAxis *_axisY; + QChart *_chart; + Callout *_tooltip; + QList _callouts; + QString _hoveredDate; + QString _dateformat; + int _numScheduledScalings; +}; + +#endif diff --git a/src/qt/customtableview.cpp b/src/qt/customtableview.cpp index 975a7bf..3ce8a25 100644 --- a/src/qt/customtableview.cpp +++ b/src/qt/customtableview.cpp @@ -7,7 +7,7 @@ #include #include #include - +#include "../Config.h" // Customizing the QTableView CustomTableView::CustomTableView(QWidget *parent) @@ -69,5 +69,5 @@ void CustomTableView::keyPressEvent(QKeyEvent *event) QString DateDelegate::displayText(const QVariant &value, const QLocale &locale) const { - return locale.toString(value.toDate(), "dd.MM.yyyy"); + return locale.toString(value.toDate(), Config::DATE_FORMAT.c_str()); }