From 0bfa633e2336e11d603b08475e65181489567530 Mon Sep 17 00:00:00 2001 From: chimp1984 Date: Sun, 31 Jan 2021 15:15:16 -0500 Subject: [PATCH 01/21] Extract onSendBsq method, return early --- .../main/dao/wallet/send/BsqSendView.java | 75 ++++++++++--------- 1 file changed, 39 insertions(+), 36 deletions(-) diff --git a/desktop/src/main/java/bisq/desktop/main/dao/wallet/send/BsqSendView.java b/desktop/src/main/java/bisq/desktop/main/dao/wallet/send/BsqSendView.java index e692f13d0f7..078b967a0e8 100644 --- a/desktop/src/main/java/bisq/desktop/main/dao/wallet/send/BsqSendView.java +++ b/desktop/src/main/java/bisq/desktop/main/dao/wallet/send/BsqSendView.java @@ -48,10 +48,10 @@ import bisq.core.locale.Res; import bisq.core.user.DontShowAgainLookup; import bisq.core.util.FormattingUtils; +import bisq.core.util.ParsingUtils; import bisq.core.util.coin.BsqFormatter; import bisq.core.util.coin.CoinFormatter; import bisq.core.util.coin.CoinUtil; -import bisq.core.util.ParsingUtils; import bisq.core.util.validation.BtcAddressValidator; import bisq.network.p2p.P2PService; @@ -242,41 +242,44 @@ private void addSendBsqGroup() { sendBsqButton = addButtonAfterGroup(root, ++gridRow, Res.get("dao.wallet.send.send")); - sendBsqButton.setOnAction((event) -> { - // TODO break up in methods - if (GUIUtil.isReadyForTxBroadcastOrShowPopup(p2PService, walletsSetup)) { - String receiversAddressString = bsqFormatter.getAddressFromBsqAddress(receiversAddressInputTextField.getText()).toString(); - Coin receiverAmount = ParsingUtils.parseToCoin(amountInputTextField.getText(), bsqFormatter); - try { - Transaction preparedSendTx = bsqWalletService.getPreparedSendBsqTx(receiversAddressString, receiverAmount); - Transaction txWithBtcFee = btcWalletService.completePreparedSendBsqTx(preparedSendTx); - Transaction signedTx = bsqWalletService.signTx(txWithBtcFee); - Coin miningFee = signedTx.getFee(); - int txVsize = signedTx.getVsize(); - showPublishTxPopup(receiverAmount, - txWithBtcFee, - TxType.TRANSFER_BSQ, - miningFee, - txVsize, - receiversAddressInputTextField.getText(), - bsqFormatter, - btcFormatter, - () -> { - receiversAddressInputTextField.setValidator(null); - receiversAddressInputTextField.setText(""); - receiversAddressInputTextField.setValidator(bsqAddressValidator); - amountInputTextField.setValidator(null); - amountInputTextField.setText(""); - amountInputTextField.setValidator(bsqValidator); - }); - } catch (BsqChangeBelowDustException e) { - String msg = Res.get("popup.warning.bsqChangeBelowDustException", bsqFormatter.formatCoinWithCode(e.getOutputValue())); - new Popup().warning(msg).show(); - } catch (Throwable t) { - handleError(t); - } - } - }); + sendBsqButton.setOnAction((event) -> onSendBsq()); + } + + private void onSendBsq() { + if (!GUIUtil.isReadyForTxBroadcastOrShowPopup(p2PService, walletsSetup)) { + return; + } + + String receiversAddressString = bsqFormatter.getAddressFromBsqAddress(receiversAddressInputTextField.getText()).toString(); + Coin receiverAmount = ParsingUtils.parseToCoin(amountInputTextField.getText(), bsqFormatter); + try { + Transaction preparedSendTx = bsqWalletService.getPreparedSendBsqTx(receiversAddressString, receiverAmount); + Transaction txWithBtcFee = btcWalletService.completePreparedSendBsqTx(preparedSendTx); + Transaction signedTx = bsqWalletService.signTx(txWithBtcFee); + Coin miningFee = signedTx.getFee(); + int txVsize = signedTx.getVsize(); + showPublishTxPopup(receiverAmount, + txWithBtcFee, + TxType.TRANSFER_BSQ, + miningFee, + txVsize, + receiversAddressInputTextField.getText(), + bsqFormatter, + btcFormatter, + () -> { + receiversAddressInputTextField.setValidator(null); + receiversAddressInputTextField.setText(""); + receiversAddressInputTextField.setValidator(bsqAddressValidator); + amountInputTextField.setValidator(null); + amountInputTextField.setText(""); + amountInputTextField.setValidator(bsqValidator); + }); + } catch (BsqChangeBelowDustException e) { + String msg = Res.get("popup.warning.bsqChangeBelowDustException", bsqFormatter.formatCoinWithCode(e.getOutputValue())); + new Popup().warning(msg).show(); + } catch (Throwable t) { + handleError(t); + } } private void setSendBtcGroupVisibleState(boolean visible) { From db86b89377a326aae6f71fa3b4c78533ce7db504 Mon Sep 17 00:00:00 2001 From: chimp1984 Date: Sun, 24 Jan 2021 20:40:34 -0500 Subject: [PATCH 02/21] Exclude proof of burn amounts from trade fee display. Use Proof of burn fee instead of burned BSQ from invalid tx. --- .../main/java/bisq/core/dao/DaoFacade.java | 8 +++- .../bisq/core/dao/state/DaoStateService.java | 14 ++++-- .../resources/i18n/displayStrings.properties | 4 +- .../main/dao/economy/supply/SupplyView.java | 47 ++++++++++--------- 4 files changed, 43 insertions(+), 30 deletions(-) diff --git a/core/src/main/java/bisq/core/dao/DaoFacade.java b/core/src/main/java/bisq/core/dao/DaoFacade.java index e5e560b2f7a..78150fc6d57 100644 --- a/core/src/main/java/bisq/core/dao/DaoFacade.java +++ b/core/src/main/java/bisq/core/dao/DaoFacade.java @@ -650,8 +650,12 @@ public Optional getLockupTxOutput(String txId) { return daoStateService.getLockupTxOutput(txId); } - public long getTotalBurntFee() { - return daoStateService.getTotalBurntFee(); + public long getTotalBurntTradeFee() { + return daoStateService.getTotalBurntTradeFee(); + } + + public long getTotalProofOfBurnAmount() { + return daoStateService.getTotalProofOfBurnAmount(); } public long getTotalIssuedAmount(IssuanceType issuanceType) { diff --git a/core/src/main/java/bisq/core/dao/state/DaoStateService.java b/core/src/main/java/bisq/core/dao/state/DaoStateService.java index 219f2862851..f3a58a118f7 100644 --- a/core/src/main/java/bisq/core/dao/state/DaoStateService.java +++ b/core/src/main/java/bisq/core/dao/state/DaoStateService.java @@ -394,7 +394,7 @@ public Optional getOptionalTxType(String txId) { /////////////////////////////////////////////////////////////////////////////////////////// - // BurntFee + // BurntFee (trade fee and fee burned at proof of burn) /////////////////////////////////////////////////////////////////////////////////////////// public long getBurntFee(String txId) { @@ -405,8 +405,16 @@ public boolean hasTxBurntFee(String txId) { return getBurntFee(txId) > 0; } - public long getTotalBurntFee() { - return getUnorderedTxStream().mapToLong(Tx::getBurntFee).sum(); + public long getTotalBurntTradeFee() { + return getUnorderedTxStream() + .filter(tx -> TxOutputType.PROOF_OF_BURN_OP_RETURN_OUTPUT != tx.getLastTxOutput().getTxOutputType()) + .mapToLong(Tx::getBurntFee).sum(); + } + + public long getTotalProofOfBurnAmount() { + return getUnorderedTxStream() + .filter(tx -> TxOutputType.PROOF_OF_BURN_OP_RETURN_OUTPUT == tx.getLastTxOutput().getTxOutputType()) + .mapToLong(Tx::getBurntFee).sum(); } public Set getBurntFeeTxs() { diff --git a/core/src/main/resources/i18n/displayStrings.properties b/core/src/main/resources/i18n/displayStrings.properties index 98513e3b324..6561963e6f1 100644 --- a/core/src/main/resources/i18n/displayStrings.properties +++ b/core/src/main/resources/i18n/displayStrings.properties @@ -2451,8 +2451,8 @@ dao.factsAndFigures.supply.totalLockedUpAmount=Locked up in bonds dao.factsAndFigures.supply.totalUnlockingAmount=Unlocking BSQ from bonds dao.factsAndFigures.supply.totalUnlockedAmount=Unlocked BSQ from bonds dao.factsAndFigures.supply.totalConfiscatedAmount=Confiscated BSQ from bonds -dao.factsAndFigures.supply.invalidTxs=Burned BSQ (invalid transactions) -dao.factsAndFigures.supply.burntAmount=Burned BSQ (fees) +dao.factsAndFigures.supply.proofOfBurnAmount=BSQ burned via 'Proof of Burn' +dao.factsAndFigures.supply.burntAmount=Burned BSQ (trade fees) dao.factsAndFigures.transactions.genesis=Genesis transaction dao.factsAndFigures.transactions.genesisBlockHeight=Genesis block height diff --git a/desktop/src/main/java/bisq/desktop/main/dao/economy/supply/SupplyView.java b/desktop/src/main/java/bisq/desktop/main/dao/economy/supply/SupplyView.java index cd3d05b9656..28507b33c4d 100644 --- a/desktop/src/main/java/bisq/desktop/main/dao/economy/supply/SupplyView.java +++ b/desktop/src/main/java/bisq/desktop/main/dao/economy/supply/SupplyView.java @@ -105,8 +105,8 @@ public class SupplyView extends ActivatableView implements DaoSt private int gridRow = 0; private TextField genesisIssueAmountTextField, compRequestIssueAmountTextField, reimbursementAmountTextField, - totalBurntFeeAmountTextField, totalLockedUpAmountTextField, totalUnlockingAmountTextField, - totalUnlockedAmountTextField, totalConfiscatedAmountTextField, totalAmountOfInvalidatedBsqTextField; + totalBurntTradeFeeTextField, totalLockedUpAmountTextField, totalUnlockingAmountTextField, + totalUnlockedAmountTextField, totalConfiscatedAmountTextField, totalProofOfBurnAmountTextField; private XYChart.Series seriesBSQIssuedMonthly, seriesBSQBurntMonthly, seriesBSQBurntDaily, seriesBSQBurntDailyMA; @@ -256,10 +256,10 @@ private void createSupplyIncreasedInformation() { private void createSupplyReducedInformation() { addTitledGroupBg(root, ++gridRow, 3, Res.get("dao.factsAndFigures.supply.burnt"), Layout.GROUP_DISTANCE); - totalBurntFeeAmountTextField = addTopLabelReadOnlyTextField(root, gridRow, + totalBurntTradeFeeTextField = addTopLabelReadOnlyTextField(root, gridRow, Res.get("dao.factsAndFigures.supply.burntAmount"), Layout.FIRST_ROW_AND_GROUP_DISTANCE).second; - totalAmountOfInvalidatedBsqTextField = addTopLabelReadOnlyTextField(root, gridRow, 1, - Res.get("dao.factsAndFigures.supply.invalidTxs"), Layout.FIRST_ROW_AND_GROUP_DISTANCE).second; + totalProofOfBurnAmountTextField = addTopLabelReadOnlyTextField(root, gridRow, 1, + Res.get("dao.factsAndFigures.supply.proofOfBurnAmount"), Layout.FIRST_ROW_AND_GROUP_DISTANCE).second; var buttonTitle = Res.get("dao.factsAndFigures.supply.burntZoomToInliers"); zoomToInliersSlide = addSlideToggleButton(root, ++gridRow, buttonTitle); @@ -289,8 +289,8 @@ private void createSupplyLockedInformation() { // chart #1 (top) private Node createBSQIssuedVsBurntChart( - XYChart.Series seriesBSQIssuedMonthly, - XYChart.Series seriesBSQBurntMonthly + XYChart.Series seriesBSQIssuedMonthly, + XYChart.Series seriesBSQBurntMonthly ) { xAxisChart1 = new NumberAxis(); configureAxis(xAxisChart1); @@ -348,7 +348,7 @@ private void initializeChangeListener(NumberAxis axis) { yAxisBSQBurntDaily, chartMaxNumberOfTicks, chartPercentToTrim, chartHowManyStdDevsConstituteOutlier); } - public static List getListXMinMax (List> bsqList) { + public static List getListXMinMax(List> bsqList) { long min = Long.MAX_VALUE, max = 0; for (XYChart.Data data : bsqList) { min = Math.min(data.getXValue().longValue(), min); @@ -374,16 +374,16 @@ private void configureAxis(NumberAxis axis) { // grab the axis tick mark label (text object) and add a CSS class. private void addTickMarkLabelCssClass(NumberAxis axis, String cssClass) { axis.getChildrenUnmodifiable().addListener((ListChangeListener) c -> { - while (c.next()) { - if (c.wasAdded()) { - for (Node mark : c.getAddedSubList()) { - if (mark instanceof Text) { - mark.getStyleClass().add(cssClass); - } + while (c.next()) { + if (c.wasAdded()) { + for (Node mark : c.getAddedSubList()) { + if (mark instanceof Text) { + mark.getStyleClass().add(cssClass); } } } - }); + } + }); } // rounds the tick timestamp to the nearest month @@ -393,8 +393,8 @@ private StringConverter getMonthTickLabelFormatter(String datePattern) { public String toString(Number timestamp) { double tsd = timestamp.doubleValue(); if ((chart1XBounds.size() == 2) && - ((tsd - monthDurationAvg / 2 < chart1XBounds.get(0).doubleValue()) || - (tsd + monthDurationAvg / 2 > chart1XBounds.get(1).doubleValue()))) { + ((tsd - monthDurationAvg / 2 < chart1XBounds.get(0).doubleValue()) || + (tsd + monthDurationAvg / 2 > chart1XBounds.get(1).doubleValue()))) { return ""; } LocalDateTime localDateTime = LocalDateTime.ofEpochSecond(timestamp.longValue(), @@ -477,20 +477,21 @@ private void updateWithBsqBlockChainData() { Coin issuedAmountFromReimbursementRequests = Coin.valueOf(daoFacade.getTotalIssuedAmount(IssuanceType.REIMBURSEMENT)); reimbursementAmountTextField.setText(bsqFormatter.formatAmountWithGroupSeparatorAndCode(issuedAmountFromReimbursementRequests)); - Coin totalBurntFee = Coin.valueOf(daoFacade.getTotalBurntFee()); + Coin totalBurntTradeFee = Coin.valueOf(daoFacade.getTotalBurntTradeFee()); + Coin totalProofOfBurnAmount = Coin.valueOf(daoFacade.getTotalProofOfBurnAmount()); Coin totalLockedUpAmount = Coin.valueOf(daoFacade.getTotalLockupAmount()); Coin totalUnlockingAmount = Coin.valueOf(daoFacade.getTotalAmountOfUnLockingTxOutputs()); Coin totalUnlockedAmount = Coin.valueOf(daoFacade.getTotalAmountOfUnLockedTxOutputs()); Coin totalConfiscatedAmount = Coin.valueOf(daoFacade.getTotalAmountOfConfiscatedTxOutputs()); - Coin totalAmountOfInvalidatedBsq = Coin.valueOf(daoFacade.getTotalAmountOfInvalidatedBsq()); - totalBurntFeeAmountTextField.setText("-" + bsqFormatter.formatAmountWithGroupSeparatorAndCode(totalBurntFee)); + String minusSign = totalBurntTradeFee.isPositive() ? "-" : ""; + totalBurntTradeFeeTextField.setText(minusSign + bsqFormatter.formatAmountWithGroupSeparatorAndCode(totalBurntTradeFee)); + minusSign = totalProofOfBurnAmount.isPositive() ? "-" : ""; + totalProofOfBurnAmountTextField.setText(minusSign + bsqFormatter.formatAmountWithGroupSeparatorAndCode(totalProofOfBurnAmount)); totalLockedUpAmountTextField.setText(bsqFormatter.formatAmountWithGroupSeparatorAndCode(totalLockedUpAmount)); totalUnlockingAmountTextField.setText(bsqFormatter.formatAmountWithGroupSeparatorAndCode(totalUnlockingAmount)); totalUnlockedAmountTextField.setText(bsqFormatter.formatAmountWithGroupSeparatorAndCode(totalUnlockedAmount)); totalConfiscatedAmountTextField.setText(bsqFormatter.formatAmountWithGroupSeparatorAndCode(totalConfiscatedAmount)); - String minusSign = totalAmountOfInvalidatedBsq.isPositive() ? "-" : ""; - totalAmountOfInvalidatedBsqTextField.setText(minusSign + bsqFormatter.formatAmountWithGroupSeparatorAndCode(totalAmountOfInvalidatedBsq)); updateChartSeries(); } @@ -506,7 +507,7 @@ private void updateChartSeries() { List xMinMaxI = updateBSQIssuedMonthly(); chart1XBounds = List.of(Math.min(xMinMaxB.get(0).doubleValue(), xMinMaxI.get(0).doubleValue()) - monthDurationAvg, - Math.max(xMinMaxB.get(1).doubleValue(), xMinMaxI.get(1).doubleValue()) + monthDurationAvg); + Math.max(xMinMaxB.get(1).doubleValue(), xMinMaxI.get(1).doubleValue()) + monthDurationAvg); xAxisChart1.setAutoRanging(false); xAxisChart1.setLowerBound(chart1XBounds.get(0).doubleValue()); xAxisChart1.setUpperBound(chart1XBounds.get(1).doubleValue()); From fcaad5580b914d69c4731c01ad046dcab3c32e0f Mon Sep 17 00:00:00 2001 From: chimp1984 Date: Sun, 24 Jan 2021 20:41:54 -0500 Subject: [PATCH 03/21] Use readableFileSize instead of division by 1024 for kb display in statistic logs --- .../main/java/bisq/network/p2p/network/Statistic.java | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/p2p/src/main/java/bisq/network/p2p/network/Statistic.java b/p2p/src/main/java/bisq/network/p2p/network/Statistic.java index 0203ccdefc3..a81ca090a29 100644 --- a/p2p/src/main/java/bisq/network/p2p/network/Statistic.java +++ b/p2p/src/main/java/bisq/network/p2p/network/Statistic.java @@ -19,6 +19,7 @@ import bisq.common.UserThread; import bisq.common.proto.network.NetworkEnvelope; +import bisq.common.util.Utilities; import javafx.beans.property.DoubleProperty; import javafx.beans.property.IntegerProperty; @@ -75,16 +76,16 @@ public class Statistic { UserThread.runPeriodically(() -> { String ls = System.lineSeparator(); log.info("Accumulated network statistics:" + ls + - "Bytes sent: {} kb;" + ls + + "Bytes sent: {};" + ls + "Number of sent messages/Sent messages: {} / {};" + ls + "Number of sent messages per sec: {};" + ls + - "Bytes received: {} kb" + ls + + "Bytes received: {}" + ls + "Number of received messages/Received messages: {} / {};" + ls + "Number of received messages per sec: {};" + ls, - totalSentBytes.get() / 1024d, + Utilities.readableFileSize(totalSentBytes.get()), numTotalSentMessages.get(), totalSentMessages, numTotalSentMessagesPerSec.get(), - totalReceivedBytes.get() / 1024d, + Utilities.readableFileSize(totalReceivedBytes.get()), numTotalReceivedMessages.get(), totalReceivedMessages, numTotalReceivedMessagesPerSec.get()); }, TimeUnit.MINUTES.toSeconds(5)); From 632653a751ef9e9179c3f2e7fc3f31ed40f21708 Mon Sep 17 00:00:00 2001 From: chimp1984 Date: Fri, 5 Feb 2021 01:23:56 -0500 Subject: [PATCH 04/21] Add new chart view --- .../main/java/bisq/core/dao/DaoFacade.java | 8 - .../bisq/core/dao/state/DaoStateService.java | 17 +- .../resources/i18n/displayStrings.properties | 11 +- desktop/src/main/java/bisq/desktop/bisq.css | 33 +- .../desktop/components/chart/ChartModel.java | 98 +++ .../desktop/components/chart/ChartView.java | 407 +++++++++++ .../supply/DaoEconomyDataProvider.java | 257 +++++++ .../main/dao/economy/supply/SupplyView.java | 683 +++--------------- .../supply/chart/DaoEconomyChartModel.java | 103 +++ .../supply/chart/DaoEconomyChartView.java | 265 +++++++ .../src/main/java/bisq/desktop/theme-dark.css | 3 + .../main/java/bisq/desktop/theme-light.css | 3 + .../java/bisq/desktop/util/FormBuilder.java | 51 +- 13 files changed, 1330 insertions(+), 609 deletions(-) create mode 100644 desktop/src/main/java/bisq/desktop/components/chart/ChartModel.java create mode 100644 desktop/src/main/java/bisq/desktop/components/chart/ChartView.java create mode 100644 desktop/src/main/java/bisq/desktop/main/dao/economy/supply/DaoEconomyDataProvider.java create mode 100644 desktop/src/main/java/bisq/desktop/main/dao/economy/supply/chart/DaoEconomyChartModel.java create mode 100644 desktop/src/main/java/bisq/desktop/main/dao/economy/supply/chart/DaoEconomyChartView.java diff --git a/core/src/main/java/bisq/core/dao/DaoFacade.java b/core/src/main/java/bisq/core/dao/DaoFacade.java index 78150fc6d57..701849b48ba 100644 --- a/core/src/main/java/bisq/core/dao/DaoFacade.java +++ b/core/src/main/java/bisq/core/dao/DaoFacade.java @@ -650,14 +650,6 @@ public Optional getLockupTxOutput(String txId) { return daoStateService.getLockupTxOutput(txId); } - public long getTotalBurntTradeFee() { - return daoStateService.getTotalBurntTradeFee(); - } - - public long getTotalProofOfBurnAmount() { - return daoStateService.getTotalProofOfBurnAmount(); - } - public long getTotalIssuedAmount(IssuanceType issuanceType) { return daoStateService.getTotalIssuedAmount(issuanceType); } diff --git a/core/src/main/java/bisq/core/dao/state/DaoStateService.java b/core/src/main/java/bisq/core/dao/state/DaoStateService.java index f3a58a118f7..77fbec18463 100644 --- a/core/src/main/java/bisq/core/dao/state/DaoStateService.java +++ b/core/src/main/java/bisq/core/dao/state/DaoStateService.java @@ -192,6 +192,10 @@ public Optional getStartHeightOfNextCycle(int blockHeight) { return getCycle(blockHeight).map(cycle -> cycle.getHeightOfLastBlock() + 1); } + public Optional getStartHeightOfCurrentCycle(int blockHeight) { + return getCycle(blockHeight).map(cycle -> cycle.getHeightOfFirstBlock()); + } + /////////////////////////////////////////////////////////////////////////////////////////// // Block @@ -405,18 +409,19 @@ public boolean hasTxBurntFee(String txId) { return getBurntFee(txId) > 0; } - public long getTotalBurntTradeFee() { + public Set getTradeFeeTxs() { return getUnorderedTxStream() - .filter(tx -> TxOutputType.PROOF_OF_BURN_OP_RETURN_OUTPUT != tx.getLastTxOutput().getTxOutputType()) - .mapToLong(Tx::getBurntFee).sum(); + .filter(tx -> tx.getTxType() == TxType.PAY_TRADE_FEE) + .collect(Collectors.toSet()); } - public long getTotalProofOfBurnAmount() { + public Set getProofOfBurnTxs() { return getUnorderedTxStream() - .filter(tx -> TxOutputType.PROOF_OF_BURN_OP_RETURN_OUTPUT == tx.getLastTxOutput().getTxOutputType()) - .mapToLong(Tx::getBurntFee).sum(); + .filter(tx -> tx.getTxType() == TxType.PROOF_OF_BURN) + .collect(Collectors.toSet()); } + // Any tx with burned BSQ public Set getBurntFeeTxs() { return getUnorderedTxStream() .filter(tx -> tx.getBurntFee() > 0) diff --git a/core/src/main/resources/i18n/displayStrings.properties b/core/src/main/resources/i18n/displayStrings.properties index 6561963e6f1..e71ea804b6a 100644 --- a/core/src/main/resources/i18n/displayStrings.properties +++ b/core/src/main/resources/i18n/displayStrings.properties @@ -2438,21 +2438,24 @@ dao.factsAndFigures.dashboard.availableAmount=Total available BSQ dao.factsAndFigures.supply.issuedVsBurnt=BSQ issued v. BSQ burnt dao.factsAndFigures.supply.issued=BSQ issued +dao.factsAndFigures.supply.compReq=Compensation requests +dao.factsAndFigures.supply.reimbursement=Reimbursement requests dao.factsAndFigures.supply.genesisIssueAmount=BSQ issued at genesis transaction dao.factsAndFigures.supply.compRequestIssueAmount=BSQ issued for compensation requests dao.factsAndFigures.supply.reimbursementAmount=BSQ issued for reimbursement requests +dao.factsAndFigures.supply.chart.tradeFee.toolTip={0}\n{1} + dao.factsAndFigures.supply.burnt=BSQ burnt -dao.factsAndFigures.supply.burntMovingAverage=15-day moving average -dao.factsAndFigures.supply.burntZoomToInliers=Zoom to inliers dao.factsAndFigures.supply.locked=Global state of locked BSQ dao.factsAndFigures.supply.totalLockedUpAmount=Locked up in bonds dao.factsAndFigures.supply.totalUnlockingAmount=Unlocking BSQ from bonds dao.factsAndFigures.supply.totalUnlockedAmount=Unlocked BSQ from bonds dao.factsAndFigures.supply.totalConfiscatedAmount=Confiscated BSQ from bonds -dao.factsAndFigures.supply.proofOfBurnAmount=BSQ burned via 'Proof of Burn' -dao.factsAndFigures.supply.burntAmount=Burned BSQ (trade fees) +dao.factsAndFigures.supply.proofOfBurn=Proof of Burn +dao.factsAndFigures.supply.bsqTradeFee=BSQ Trade fees +dao.factsAndFigures.supply.btcTradeFee=BTC Trade fees dao.factsAndFigures.transactions.genesis=Genesis transaction dao.factsAndFigures.transactions.genesisBlockHeight=Genesis block height diff --git a/desktop/src/main/java/bisq/desktop/bisq.css b/desktop/src/main/java/bisq/desktop/bisq.css index bcd6c18cc92..9e16c8a270b 100644 --- a/desktop/src/main/java/bisq/desktop/bisq.css +++ b/desktop/src/main/java/bisq/desktop/bisq.css @@ -1735,8 +1735,33 @@ textfield */ #charts-dao .default-color1.chart-series-line { -fx-stroke: -bs-chart-dao-line2; } #charts-dao .default-color1.chart-line-symbol { -fx-background-color: -bs-chart-dao-line2, -bs-chart-dao-line2; } +#charts-dao .default-color2.chart-series-line { -fx-stroke: -bs-chart-dao-line3; } +#charts-dao .default-color2.chart-line-symbol { -fx-background-color: -bs-chart-dao-line3, -bs-chart-dao-line3; } + +#charts-dao .default-color3.chart-series-line { -fx-stroke: -bs-chart-dao-line4; } +#charts-dao .default-color3.chart-line-symbol { -fx-background-color: -bs-chart-dao-line4, -bs-chart-dao-line4; } + +#charts-dao .default-color4.chart-series-line { -fx-stroke: -bs-chart-dao-line5; } +#charts-dao .default-color4.chart-line-symbol { -fx-background-color: -bs-chart-dao-line5, -bs-chart-dao-line5; } + +#charts-legend-toggle0 { + -jfx-toggle-color: -bs-chart-dao-line1 +} +#charts-legend-toggle1 { + -jfx-toggle-color: -bs-chart-dao-line2; +} +#charts-legend-toggle2 { + -jfx-toggle-color: -bs-chart-dao-line3; +} +#charts-legend-toggle3 { + -jfx-toggle-color: -bs-chart-dao-line4; +} +#charts-legend-toggle4 { + -jfx-toggle-color: -bs-chart-dao-line5; +} + #charts-dao .chart-series-line { - -fx-stroke-width: 3px; + -fx-stroke-width: 2px; } #charts .default-color0.chart-series-area-fill { @@ -1759,6 +1784,12 @@ textfield */ -fx-alignment: center; } +#chart-navigation-label { + -fx-tick-label-fill: -bs-rd-font-lighter; + -fx-font-size: 0.769em; + -fx-alignment: center; +} + /******************************************************************************************************************** * * * Highlight buttons * diff --git a/desktop/src/main/java/bisq/desktop/components/chart/ChartModel.java b/desktop/src/main/java/bisq/desktop/components/chart/ChartModel.java new file mode 100644 index 00000000000..1c0ce459345 --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/components/chart/ChartModel.java @@ -0,0 +1,98 @@ +/* + * This file is part of Bisq. + * + * Bisq is free software: you can redistribute it and/or modify it + * under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at + * your option) any later version. + * + * Bisq is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public + * License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Bisq. If not, see . + */ + +package bisq.desktop.components.chart; + +import bisq.desktop.common.model.ActivatableViewModel; + +import bisq.common.util.Tuple2; + +import java.util.HashSet; +import java.util.Set; +import java.util.function.Predicate; + +import lombok.extern.slf4j.Slf4j; + +@Slf4j +public class ChartModel extends ActivatableViewModel { + public interface Listener { + /** + * @param fromDate Epoch date in millis for earliest data + * @param toDate Epoch date in millis for latest data + */ + void onDateFilterChanged(long fromDate, long toDate); + } + + protected Number lowerBound, upperBound; + protected final Set listeners = new HashSet<>(); + + public ChartModel() { + } + + /////////////////////////////////////////////////////////////////////////////////////////// + // Lifecycle + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public void activate() { + } + + @Override + public void deactivate() { + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // API + /////////////////////////////////////////////////////////////////////////////////////////// + + public void addListener(Listener listener) { + listeners.add(listener); + } + + public void removeListener(Listener listener) { + listeners.remove(listener); + } + + public Number getLowerBound() { + return lowerBound; + } + + public Number getUpperBound() { + return upperBound; + } + + Tuple2 timelinePositionToEpochSeconds(double leftPos, double rightPos) { + long lowerBoundAsLong = lowerBound.longValue(); + long totalRange = upperBound.longValue() - lowerBoundAsLong; + double fromDateSec = lowerBoundAsLong + totalRange * leftPos; + double toDateSec = lowerBoundAsLong + totalRange * rightPos; + return new Tuple2<>(fromDateSec, toDateSec); + } + + Predicate getPredicate(Tuple2 fromToTuple) { + return value -> value >= fromToTuple.first && value <= fromToTuple.second; + } + + void notifyListeners(Tuple2 fromToTuple) { + // We use millis for our listeners + long first = fromToTuple.first.longValue() * 1000; + long second = fromToTuple.second.longValue() * 1000; + listeners.forEach(l -> l.onDateFilterChanged(first, second)); + } + +} diff --git a/desktop/src/main/java/bisq/desktop/components/chart/ChartView.java b/desktop/src/main/java/bisq/desktop/components/chart/ChartView.java new file mode 100644 index 00000000000..6fa3848940f --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/components/chart/ChartView.java @@ -0,0 +1,407 @@ +/* + * This file is part of Bisq. + * + * Bisq is free software: you can redistribute it and/or modify it + * under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at + * your option) any later version. + * + * Bisq is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public + * License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Bisq. If not, see . + */ + +package bisq.desktop.components.chart; + +import bisq.desktop.common.view.ActivatableView; +import bisq.desktop.components.AutoTooltipSlideToggleButton; + +import bisq.common.util.Tuple2; + +import javafx.scene.Node; +import javafx.scene.chart.Axis; +import javafx.scene.chart.LineChart; +import javafx.scene.chart.NumberAxis; +import javafx.scene.chart.XYChart; +import javafx.scene.control.Label; +import javafx.scene.control.SplitPane; +import javafx.scene.control.Tooltip; +import javafx.scene.input.MouseEvent; +import javafx.scene.layout.HBox; +import javafx.scene.layout.Pane; +import javafx.scene.layout.Priority; +import javafx.scene.layout.Region; +import javafx.scene.layout.VBox; + +import javafx.geometry.Insets; + +import javafx.beans.value.ChangeListener; + +import javafx.util.StringConverter; + +import java.text.DateFormat; +import java.text.SimpleDateFormat; + +import java.util.ArrayList; +import java.util.Date; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.function.Predicate; +import java.util.stream.Stream; + +import lombok.extern.slf4j.Slf4j; + +@Slf4j +public abstract class ChartView extends ActivatableView { + private final Pane center; + private final SplitPane splitPane; + protected final NumberAxis xAxis; + protected final NumberAxis yAxis; + protected final XYChart chart; + protected final Map seriesIndexMap = new HashMap<>(); + protected final Map toggleBySeriesName = new HashMap<>(); + private final HBox timelineLabels; + private final List dividerNodes = new ArrayList<>(); + private final Double[] dividerPositions = new Double[]{0d, 1d}; + private HBox legendBox; + private boolean pressed; + private double x; + private ChangeListener widthListener; + private int maxSeriesSize; + + public ChartView(T model) { + super(model); + + root = new VBox(); + Pane left = new Pane(); + center = new Pane(); + Pane right = new Pane(); + splitPane = new SplitPane(); + splitPane.getItems().addAll(left, center, right); + + xAxis = getXAxis(); + yAxis = getYAxis(); + + chart = getChart(); + + + addSeries(); + addLegend(); + timelineLabels = new HBox(); + + VBox box = new VBox(); + int paddingRight = 60; + VBox.setMargin(splitPane, new Insets(20, paddingRight, 0, 0)); + VBox.setMargin(timelineLabels, new Insets(0, paddingRight, 0, 0)); + VBox.setMargin(legendBox, new Insets(10, paddingRight, 0, 0)); + box.getChildren().addAll(splitPane, timelineLabels, legendBox); + + VBox vBox = new VBox(); + vBox.setSpacing(10); + vBox.getChildren().addAll(chart, box); + + root.getChildren().addAll(chart, box); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Lifecycle + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public void initialize() { + center.setStyle("-fx-background-color: #dddddd"); + + splitPane.setMinHeight(30); + splitPane.setDividerPosition(0, dividerPositions[0]); + splitPane.setDividerPosition(1, dividerPositions[1]); + + widthListener = (observable, oldValue, newValue) -> { + splitPane.setDividerPosition(0, dividerPositions[0]); + splitPane.setDividerPosition(1, dividerPositions[1]); + }; + } + + @Override + public void activate() { + root.widthProperty().addListener(widthListener); + splitPane.setOnMousePressed(this::onMousePressedSplitPane); + splitPane.setOnMouseDragged(this::onMouseDragged); + center.setOnMousePressed(this::onMousePressedCenter); + center.setOnMouseReleased(this::onMouseReleasedCenter); + + initData(); + + initDividerMouseHandlers(); + } + + @Override + public void deactivate() { + root.widthProperty().removeListener(widthListener); + splitPane.setOnMousePressed(null); + splitPane.setOnMouseDragged(null); + center.setOnMousePressed(null); + center.setOnMouseReleased(null); + + dividerNodes.forEach(node -> node.setOnMouseReleased(null)); + } + + public void addListener(ChartModel.Listener listener) { + model.addListener(listener); + } + + public void removeListener(ChartModel.Listener listener) { + model.removeListener(listener); + } + + /////////////////////////////////////////////////////////////////////////////////////////// + // Customisations + /////////////////////////////////////////////////////////////////////////////////////////// + + protected NumberAxis getXAxis() { + NumberAxis xAxis; + xAxis = new NumberAxis(); + xAxis.setForceZeroInRange(false); + xAxis.setTickLabelFormatter(getTimeAxisStringConverter()); + xAxis.setAutoRanging(true); + return xAxis; + } + + protected NumberAxis getYAxis() { + NumberAxis yAxis; + yAxis = new NumberAxis(); + yAxis.setForceZeroInRange(false); + yAxis.setTickLabelFormatter(getYAxisStringConverter()); + return yAxis; + } + + protected XYChart getChart() { + LineChart chart = new LineChart<>(xAxis, yAxis); + chart.setLegendVisible(false); + chart.setAnimated(false); + return chart; + } + + protected abstract void addSeries(); + + protected void addLegend() { + legendBox = new HBox(); + legendBox.setSpacing(10); + Region spacer = new Region(); + HBox.setHgrow(spacer, Priority.ALWAYS); + legendBox.getChildren().add(spacer); + + chart.getData().forEach(series -> { + AutoTooltipSlideToggleButton toggle = new AutoTooltipSlideToggleButton(); + String seriesName = series.getName(); + toggleBySeriesName.put(seriesName, toggle); + toggle.setText(seriesName); + toggle.setId("charts-legend-toggle" + seriesIndexMap.get(seriesName)); + toggle.setSelected(true); + toggle.setOnAction(e -> onSelectToggle(series, toggle.isSelected())); + legendBox.getChildren().add(toggle); + }); + spacer = new Region(); + HBox.setHgrow(spacer, Priority.ALWAYS); + legendBox.getChildren().add(spacer); + } + + private void onSelectToggle(XYChart.Series series, boolean isSelected) { + if (isSelected) { + chart.getData().add(series); + } else { + chart.getData().remove(series); + } + applySeriesStyles(); + } + + protected void hideSeries(XYChart.Series series) { + toggleBySeriesName.get(series.getName()).setSelected(false); + onSelectToggle(series, false); + } + + protected StringConverter getTimeAxisStringConverter() { + return new StringConverter<>() { + @Override + public String toString(Number value) { + DateFormat f = new SimpleDateFormat("YYYY-MM"); + Date date = new Date(Math.round(value.doubleValue()) * 1000); + return f.format(date); + } + + @Override + public Number fromString(String string) { + return null; + } + }; + } + + protected StringConverter getYAxisStringConverter() { + return new StringConverter<>() { + @Override + public String toString(Number value) { + return String.valueOf(value); + } + + @Override + public Number fromString(String string) { + return null; + } + }; + } + + protected abstract void initData(); + + protected abstract void updateData(Predicate predicate); + + protected void applyTooltip() { + chart.getData().forEach(series -> { + series.getData().forEach(data -> { + String xValue = getXAxis().getTickLabelFormatter().toString(data.getXValue()); + String yValue = getYAxis().getTickLabelFormatter().toString(data.getYValue()); + Node node = data.getNode(); + Tooltip.install(node, new Tooltip(yValue + "\n" + xValue)); + + //Adding class on hover + node.setOnMouseEntered(event -> node.getStyleClass().add("onHover")); + + //Removing class on exit + node.setOnMouseExited(event -> node.getStyleClass().remove("onHover")); + }); + }); + } + + // Only called once when initial data are applied. We want the min. and max. values so we have the max. scale for + // navigation. + protected void setTimeLineLabels() { + timelineLabels.getChildren().clear(); + int size = xAxis.getTickMarks().size(); + for (int i = 0; i < size; i++) { + Axis.TickMark tickMark = xAxis.getTickMarks().get(i); + Number xValue = tickMark.getValue(); + String xValueString; + if (xAxis.getTickLabelFormatter() != null) { + xValueString = xAxis.getTickLabelFormatter().toString(xValue); + } else { + xValueString = String.valueOf(xValue); + } + Label label = new Label(xValueString); + label.setId("chart-navigation-label"); + Region spacer = new Region(); + HBox.setHgrow(spacer, Priority.ALWAYS); + if (i < size - 1) { + timelineLabels.getChildren().addAll(label, spacer); + } else { + // After last label we don't add a spacer + timelineLabels.getChildren().add(label); + } + } + } + + // The chart framework assigns the colored depending on the order it got added, but want to keep colors + // the same so they match with the legend toggle. + private void applySeriesStyles() { + for (int index = 0; index < chart.getData().size(); index++) { + XYChart.Series series = chart.getData().get(index); + int staticIndex = seriesIndexMap.get(series.getName()); + Set lines = getNodesForStyle(series.getNode(), ".default-color%d.chart-series-line"); + Stream symbols = series.getData().stream().map(XYChart.Data::getNode) + .flatMap(node -> getNodesForStyle(node, ".default-color%d.chart-line-symbol").stream()); + Stream.concat(lines.stream(), symbols).forEach(node -> { + removeStyles(node); + node.getStyleClass().add("default-color" + staticIndex); + }); + } + } + + private void removeStyles(Node node) { + for (int i = 0; i < getMaxSeriesSize(); i++) { + node.getStyleClass().remove("default-color" + i); + } + } + + private Set getNodesForStyle(Node node, String style) { + Set result = new HashSet<>(); + for (int i = 0; i < getMaxSeriesSize(); i++) { + result.addAll(node.lookupAll(String.format(style, i))); + } + return result; + } + + private int getMaxSeriesSize() { + maxSeriesSize = Math.max(maxSeriesSize, chart.getData().size()); + return maxSeriesSize; + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Timeline navigation + /////////////////////////////////////////////////////////////////////////////////////////// + + private void onTimelineChanged() { + double leftPos = splitPane.getDividerPositions()[0]; + double rightPos = splitPane.getDividerPositions()[1]; + // We need to snap into the 0 and 1 values once we are close as otherwise once navigation has been used we + // would not get back to exact 0 or 1. Not clear why but might be rounding issues from values at x positions of + // drag operations. + if (leftPos < 0.01) { + leftPos = 0; + } + if (rightPos > 0.99) { + rightPos = 1; + } + dividerPositions[0] = leftPos; + dividerPositions[1] = rightPos; + splitPane.setDividerPositions(leftPos, rightPos); + Tuple2 fromToTuple = model.timelinePositionToEpochSeconds(leftPos, rightPos); + updateData(model.getPredicate(fromToTuple)); + applySeriesStyles(); + model.notifyListeners(fromToTuple); + } + + private void initDividerMouseHandlers() { + // No API access to dividers ;-( only via css lookup hack (https://stackoverflow.com/questions/40707295/how-to-add-listener-to-divider-position?rq=1) + // Need to be done after added to scene and call requestLayout and applyCss. We keep it in a list atm + // and set action handler in activate. + splitPane.requestLayout(); + splitPane.applyCss(); + for (Node node : splitPane.lookupAll(".split-pane-divider")) { + dividerNodes.add(node); + node.setOnMouseReleased(e -> onTimelineChanged()); + } + } + + private void onMouseDragged(MouseEvent e) { + if (pressed) { + double newX = e.getX(); + double width = splitPane.getWidth(); + double relativeDelta = (x - newX) / width; + double leftPos = splitPane.getDividerPositions()[0] - relativeDelta; + double rightPos = splitPane.getDividerPositions()[1] - relativeDelta; + dividerPositions[0] = leftPos; + dividerPositions[1] = rightPos; + splitPane.setDividerPositions(leftPos, rightPos); + x = newX; + } + } + + private void onMouseReleasedCenter(MouseEvent e) { + pressed = false; + onTimelineChanged(); + } + + private void onMousePressedSplitPane(MouseEvent e) { + x = e.getX(); + } + + private void onMousePressedCenter(MouseEvent e) { + pressed = true; + } +} diff --git a/desktop/src/main/java/bisq/desktop/main/dao/economy/supply/DaoEconomyDataProvider.java b/desktop/src/main/java/bisq/desktop/main/dao/economy/supply/DaoEconomyDataProvider.java new file mode 100644 index 00000000000..69efddfec2f --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/main/dao/economy/supply/DaoEconomyDataProvider.java @@ -0,0 +1,257 @@ +/* + * This file is part of Bisq. + * + * Bisq is free software: you can redistribute it and/or modify it + * under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at + * your option) any later version. + * + * Bisq is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public + * License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Bisq. If not, see . + */ + +package bisq.desktop.main.dao.economy.supply; + +import bisq.core.dao.state.DaoStateService; +import bisq.core.dao.state.model.blockchain.Tx; +import bisq.core.dao.state.model.governance.Issuance; +import bisq.core.dao.state.model.governance.IssuanceType; + +import javax.inject.Inject; +import javax.inject.Singleton; + +import java.time.Instant; +import java.time.ZoneId; +import java.time.temporal.TemporalAdjuster; +import java.time.temporal.TemporalAdjusters; + +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.function.Function; +import java.util.function.Predicate; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import lombok.extern.slf4j.Slf4j; + +@Slf4j +@Singleton +public class DaoEconomyDataProvider { + private static final ZoneId ZONE_ID = ZoneId.systemDefault(); + private static final TemporalAdjuster FIRST_DAY_OF_MONTH = TemporalAdjusters.firstDayOfMonth(); + + private final DaoStateService daoStateService; + private final Function blockHeightToEpochSeconds; + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Constructor + /////////////////////////////////////////////////////////////////////////////////////////// + + @Inject + public DaoEconomyDataProvider(DaoStateService daoStateService) { + super(); + this.daoStateService = daoStateService; + + blockHeightToEpochSeconds = memoize(height -> + toStartOfMonth(Instant.ofEpochMilli(daoStateService.getBlockTime(height)))); + } + + /** + * @param fromDate Epoch in millis + * @param toDate Epoch in millis + */ + public long getCompensationAmount(long fromDate, long toDate) { + return getMergedCompensationMap(getPredicate(fromDate, toDate)).values().stream() + .mapToLong(e -> e) + .sum(); + } + + /** + * @param fromDate Epoch in millis + * @param toDate Epoch in millis + */ + public long getReimbursementAmount(long fromDate, long toDate) { + return getMergedReimbursementMap(getPredicate(fromDate, toDate)).values().stream() + .mapToLong(e -> e) + .sum(); + } + + /** + * @param fromDate Epoch in millis + * @param toDate Epoch in millis + */ + public long getBsqTradeFeeAmount(long fromDate, long toDate) { + return getBurnedBsqByMonth(daoStateService.getTradeFeeTxs(), getPredicate(fromDate, toDate)).values() + .stream() + .mapToLong(e -> e) + .sum(); + } + + /** + * @param fromDate Epoch in millis + * @param toDate Epoch in millis + */ + /* public long getBtcTradeFeeAmount(long fromDate, long toDate) { + return getBurnedBtcByMonth(getPredicate(fromDate, toDate)).values() + .stream() + .mapToLong(e -> e) + .sum(); + }*/ + + /** + * @param fromDate Epoch in millis + * @param toDate Epoch in millis + */ + public long getProofOfBurnAmount(long fromDate, long toDate) { + return getBurnedBsqByMonth(daoStateService.getProofOfBurnTxs(), getPredicate(fromDate, toDate)).values().stream() + .mapToLong(e -> e) + .sum(); + } + + public Map getBurnedBsqByMonth(Collection txs, Predicate predicate) { + return txs.stream() + .collect(Collectors.groupingBy(tx -> toStartOfMonth(Instant.ofEpochMilli(tx.getTime())))) + .entrySet() + .stream() + .filter(entry -> predicate.test(entry.getKey())) + .collect(Collectors.toMap(Map.Entry::getKey, + entry -> entry.getValue().stream() + .mapToLong(Tx::getBurntBsq) + .sum())); + } + + // We map all issuance entries to a map with the beginning of the month as key and the list of issuance as value. + // Then we apply the date filter and and sum up all issuance amounts if the items in the list to return the + // issuance by month. We use calendar month because we want to combine the data with other data and using the cycle + // as adjuster would be more complicate (though could be done in future). + public Map getIssuedBsqByMonth(Set issuanceSet, Predicate predicate) { + return issuanceSet.stream() + .collect(Collectors.groupingBy(blockHeightToEpochSeconds.compose(issuance -> + daoStateService.getStartHeightOfCurrentCycle(issuance.getChainHeight()).orElse(0)))) + .entrySet() + .stream() + .filter(entry -> predicate.test(entry.getKey())) + .collect(Collectors.toMap(Map.Entry::getKey, + entry -> entry.getValue().stream() + .mapToLong(Issuance::getAmount) + .sum())); + } + + public Map getMergedCompensationMap(Predicate predicate) { + return getMergedMap(daoStateService.getIssuanceSetForType(IssuanceType.COMPENSATION), + DaoEconomyHistoricalData.COMPENSATIONS_BY_CYCLE_DATE, + predicate); + } + + public Map getMergedReimbursementMap(Predicate predicate) { + return getMergedMap(daoStateService.getIssuanceSetForType(IssuanceType.REIMBURSEMENT), + DaoEconomyHistoricalData.REIMBURSEMENTS_BY_CYCLE_DATE, + predicate); + } + + private Map getMergedMap(Set issuanceSet, + Map historicalData, + Predicate predicate) { + // We did not use the reimbursement requests initially (but the compensation requests) because the limits + // have been too low. Over time it got mixed in compensation requests and reimbursement requests. + // To reflect that we use static data derived from the Github data. For new data we do not need that anymore + // as we have clearly separated that now. In case we have duplicate data for a months we use the static data. + Map historicalDataMap = historicalData.entrySet().stream() + .filter(e -> predicate.test(e.getKey())) + .collect(Collectors.toMap(e -> toStartOfMonth(Instant.ofEpochSecond(e.getKey())), + Map.Entry::getValue)); + + // We merge both maps. + // If we have 2 values at same key we use the staticData as that include the daoData + return Stream.concat(getIssuedBsqByMonth(issuanceSet, predicate).entrySet().stream(), + historicalDataMap.entrySet().stream()) + .collect(Collectors.toMap(Map.Entry::getKey, + Map.Entry::getValue, + (issuedBsqByMonthValue, staticDataValue) -> staticDataValue)); + } + + // The resulting data are not very useful. We might drop that.... + /* public Map getBurnedBtcByMonth(Predicate predicate) { + Map issuedBsqByMonth = getMergedReimbursementMap(predicate); + Map burnedBsqByMonth = getBurnedBsqByMonth(daoStateService.getProofOfBurnTxs(), predicate); + return Stream.concat(issuedBsqByMonth.entrySet().stream(), + burnedBsqByMonth.entrySet().stream()) + .collect(Collectors.toMap(Map.Entry::getKey, + Map.Entry::getValue, + (issued, burned) -> burned - issued)); + }*/ + + public static long toStartOfMonth(Instant instant) { + return instant + .atZone(ZONE_ID) + .toLocalDate() + .with(FIRST_DAY_OF_MONTH) + .atStartOfDay(ZONE_ID) + .toInstant() + .getEpochSecond(); + } + + /////////////////////////////////////////////////////////////////////////////////////////// + // Utils + /////////////////////////////////////////////////////////////////////////////////////////// + + private static Predicate getPredicate(long fromDate, long toDate) { + return value -> value >= fromDate / 1000 && value <= toDate / 1000; + } + + private static Function memoize(Function fn) { + Map map = new ConcurrentHashMap<>(); + return x -> map.computeIfAbsent(x, fn); + } + + private static class DaoEconomyHistoricalData { + // Key is start date of the cycle in epoch seconds, value is reimbursement amount + public final static Map REIMBURSEMENTS_BY_CYCLE_DATE = new HashMap<>(); + public final static Map COMPENSATIONS_BY_CYCLE_DATE = new HashMap<>(); + + static { + REIMBURSEMENTS_BY_CYCLE_DATE.put(1571349571L, 60760L); + REIMBURSEMENTS_BY_CYCLE_DATE.put(1574180991L, 2621000L); + REIMBURSEMENTS_BY_CYCLE_DATE.put(1576966522L, 4769100L); + REIMBURSEMENTS_BY_CYCLE_DATE.put(1579613568L, 0L); + REIMBURSEMENTS_BY_CYCLE_DATE.put(1582399054L, 9186600L); + REIMBURSEMENTS_BY_CYCLE_DATE.put(1585342220L, 12089400L); + REIMBURSEMENTS_BY_CYCLE_DATE.put(1588025030L, 5420700L); + REIMBURSEMENTS_BY_CYCLE_DATE.put(1591004931L, 9138760L); + REIMBURSEMENTS_BY_CYCLE_DATE.put(1593654027L, 7407637L); + REIMBURSEMENTS_BY_CYCLE_DATE.put(1596407074L, 2160157L); + REIMBURSEMENTS_BY_CYCLE_DATE.put(1599175867L, 8769408L); + REIMBURSEMENTS_BY_CYCLE_DATE.put(1601861442L, 4956585L); + REIMBURSEMENTS_BY_CYCLE_DATE.put(1604845863L, 2121664L); + + COMPENSATIONS_BY_CYCLE_DATE.put(1555340856L, 6931863L); + COMPENSATIONS_BY_CYCLE_DATE.put(1558083590L, 2287000L); + COMPENSATIONS_BY_CYCLE_DATE.put(1560771266L, 2273000L); + COMPENSATIONS_BY_CYCLE_DATE.put(1563347672L, 2943772L); + COMPENSATIONS_BY_CYCLE_DATE.put(1566009595L, 10040170L); + COMPENSATIONS_BY_CYCLE_DATE.put(1568643566L, 8685115L); + COMPENSATIONS_BY_CYCLE_DATE.put(1571349571L, 7315879L); + COMPENSATIONS_BY_CYCLE_DATE.put(1574180991L, 12508300L); + COMPENSATIONS_BY_CYCLE_DATE.put(1576966522L, 5884500L); + COMPENSATIONS_BY_CYCLE_DATE.put(1579613568L, 8206000L); + COMPENSATIONS_BY_CYCLE_DATE.put(1582399054L, 3518364L); + COMPENSATIONS_BY_CYCLE_DATE.put(1585342220L, 6231700L); + COMPENSATIONS_BY_CYCLE_DATE.put(1588025030L, 4391400L); + COMPENSATIONS_BY_CYCLE_DATE.put(1591004931L, 3636463L); + COMPENSATIONS_BY_CYCLE_DATE.put(1593654027L, 6156631L); + COMPENSATIONS_BY_CYCLE_DATE.put(1596407074L, 5838368L); + COMPENSATIONS_BY_CYCLE_DATE.put(1599175867L, 6086442L); + COMPENSATIONS_BY_CYCLE_DATE.put(1601861442L, 5615973L); + COMPENSATIONS_BY_CYCLE_DATE.put(1604845863L, 7782667L); + } + } +} diff --git a/desktop/src/main/java/bisq/desktop/main/dao/economy/supply/SupplyView.java b/desktop/src/main/java/bisq/desktop/main/dao/economy/supply/SupplyView.java index 28507b33c4d..9878e6822ca 100644 --- a/desktop/src/main/java/bisq/desktop/main/dao/economy/supply/SupplyView.java +++ b/desktop/src/main/java/bisq/desktop/main/dao/economy/supply/SupplyView.java @@ -20,18 +20,13 @@ import bisq.desktop.common.view.ActivatableView; import bisq.desktop.common.view.FxmlView; import bisq.desktop.components.TitledGroupBg; -import bisq.desktop.util.AxisInlierUtils; +import bisq.desktop.components.chart.ChartModel; +import bisq.desktop.main.dao.economy.supply.chart.DaoEconomyChartView; import bisq.desktop.util.Layout; -import bisq.desktop.util.MovingAverageUtils; import bisq.core.dao.DaoFacade; import bisq.core.dao.state.DaoStateListener; -import bisq.core.dao.state.DaoStateService; import bisq.core.dao.state.model.blockchain.Block; -import bisq.core.dao.state.model.blockchain.Tx; -import bisq.core.dao.state.model.governance.Issuance; -import bisq.core.dao.state.model.governance.IssuanceType; -import bisq.core.locale.GlobalSettings; import bisq.core.locale.Res; import bisq.core.util.coin.BsqFormatter; @@ -41,91 +36,34 @@ import javax.inject.Inject; -import javafx.scene.Node; -import javafx.scene.chart.LineChart; -import javafx.scene.chart.NumberAxis; -import javafx.scene.chart.XYChart; import javafx.scene.control.Label; import javafx.scene.control.TextField; -import javafx.scene.control.ToggleButton; import javafx.scene.layout.AnchorPane; import javafx.scene.layout.GridPane; -import javafx.scene.layout.Pane; import javafx.scene.layout.VBox; -import javafx.scene.text.Text; import javafx.geometry.Insets; -import javafx.geometry.Side; - -import javafx.collections.ListChangeListener; - -import javafx.util.StringConverter; - -import java.time.Instant; -import java.time.LocalDate; -import java.time.LocalDateTime; -import java.time.OffsetDateTime; -import java.time.ZoneId; -import java.time.ZonedDateTime; -import java.time.format.DateTimeFormatter; -import java.time.temporal.TemporalAdjuster; -import java.time.temporal.TemporalAdjusters; - -import java.text.DecimalFormat; - -import java.util.Comparator; -import java.util.HashMap; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.Spliterators.AbstractSpliterator; -import java.util.concurrent.ConcurrentHashMap; -import java.util.function.BiFunction; -import java.util.function.Consumer; -import java.util.function.Function; -import java.util.stream.Collectors; -import java.util.stream.Stream; -import java.util.stream.StreamSupport; - -import static bisq.desktop.util.FormBuilder.addSlideToggleButton; + +import java.util.Date; + import static bisq.desktop.util.FormBuilder.addTitledGroupBg; import static bisq.desktop.util.FormBuilder.addTopLabelReadOnlyTextField; @FxmlView -public class SupplyView extends ActivatableView implements DaoStateListener { - - private static final String MONTH = "month"; - private static final String DAY = "day"; - private static final DecimalFormat dFmt = new DecimalFormat(",###"); - +public class SupplyView extends ActivatableView implements DaoStateListener, ChartModel.Listener { private final DaoFacade daoFacade; - private DaoStateService daoStateService; + private final DaoEconomyChartView daoEconomyChartView; + // Shared model between SupplyView and RevenueChartModel + private final DaoEconomyDataProvider daoEconomyDataProvider; private final BsqFormatter bsqFormatter; - private int gridRow = 0; private TextField genesisIssueAmountTextField, compRequestIssueAmountTextField, reimbursementAmountTextField, - totalBurntTradeFeeTextField, totalLockedUpAmountTextField, totalUnlockingAmountTextField, + totalBurntBsqTradeFeeTextField, /*totalBurntBtcTradeFeeTextField, */ + totalLockedUpAmountTextField, totalUnlockingAmountTextField, totalUnlockedAmountTextField, totalConfiscatedAmountTextField, totalProofOfBurnAmountTextField; - private XYChart.Series seriesBSQIssuedMonthly, seriesBSQBurntMonthly, seriesBSQBurntDaily, - seriesBSQBurntDailyMA; - - private ListChangeListener> changeListenerBSQBurntDaily; - private NumberAxis yAxisBSQBurntDaily; - - private ToggleButton zoomToInliersSlide; - private boolean isZoomingToInliers = false; - - // Parameters for zooming to inliers; explanations in AxisInlierUtils. - private int chartMaxNumberOfTicks = 10; - private double chartPercentToTrim = 5; - private double chartHowManyStdDevsConstituteOutlier = 10; - - private static final Map ADJUSTERS = new HashMap<>(); + private int gridRow = 0; + private long fromDate, toDate; - private static final double monthDurationAvg = 2635200; // 3600 * 24 * 30.5; - private static List chart1XBounds = List.of(); - private static NumberAxis xAxisChart1; /////////////////////////////////////////////////////////////////////////////////////////// // Constructor, lifecycle @@ -133,47 +71,54 @@ public class SupplyView extends ActivatableView implements DaoSt @Inject private SupplyView(DaoFacade daoFacade, - DaoStateService daoStateService, + DaoEconomyChartView daoEconomyChartView, + DaoEconomyDataProvider daoEconomyDataProvider, BsqFormatter bsqFormatter) { this.daoFacade = daoFacade; - this.daoStateService = daoStateService; + this.daoEconomyChartView = daoEconomyChartView; + this.daoEconomyDataProvider = daoEconomyDataProvider; this.bsqFormatter = bsqFormatter; } @Override public void initialize() { - ADJUSTERS.put(MONTH, TemporalAdjusters.firstDayOfMonth()); - ADJUSTERS.put(DAY, TemporalAdjusters.ofDateAdjuster(d -> d)); - - initializeSeries(); - - createSupplyIncreasedVsDecreasedInformation(); // chart #1 - createSupplyIncreasedInformation(); // chart #2 - createSupplyReducedInformation(); // chart #3 + daoFacade.getTx(daoFacade.getGenesisTxId()).ifPresent(tx -> fromDate = tx.getTime()); - createSupplyLockedInformation(); + createChart(); + createIssuedAndBurnedFields(); + createLockedBsqFields(); } @Override protected void activate() { - daoFacade.addBsqStateListener(this); - - if (isZoomingToInliers) { - activateZoomingToInliers(); - } + Coin issuedAmountFromGenesis = daoFacade.getGenesisTotalSupply(); + genesisIssueAmountTextField.setText(bsqFormatter.formatAmountWithGroupSeparatorAndCode(issuedAmountFromGenesis)); updateWithBsqBlockChainData(); - activateButtons(); + daoEconomyChartView.activate(); + daoEconomyChartView.addListener(this); + daoFacade.addBsqStateListener(this); } @Override protected void deactivate() { + daoEconomyChartView.removeListener(this); daoFacade.removeBsqStateListener(this); + } - deactivateZoomingToInliers(); - deactivateButtons(); + /////////////////////////////////////////////////////////////////////////////////////////// + // ChartModel.Listener + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public void onDateFilterChanged(long fromDate, long toDate) { + this.fromDate = fromDate; + this.toDate = toDate; + log.error(new Date(fromDate).toString()); + log.error(new Date(toDate).toString()); + updateEconomicsData(); } @@ -186,61 +131,32 @@ public void onParseBlockCompleteAfterBatchProcessing(Block block) { updateWithBsqBlockChainData(); } - /////////////////////////////////////////////////////////////////////////////////////////// - // Private + // Build UI /////////////////////////////////////////////////////////////////////////////////////////// - private void initializeSeries() { - // We can use the same labels for daily and monthly series - var issuedLabel = Res.get("dao.factsAndFigures.supply.issued"); - var burntLabel = Res.get("dao.factsAndFigures.supply.burnt"); - - seriesBSQIssuedMonthly = new XYChart.Series<>(); - seriesBSQIssuedMonthly.setName(issuedLabel); - - // Because Series cannot be reused in multiple charts, we create a - // "second" Series and populate it at the same time as the original. - // Some other solutions: https://stackoverflow.com/questions/49770442 - - seriesBSQBurntMonthly = new XYChart.Series<>(); - seriesBSQBurntMonthly.setName(burntLabel); - - seriesBSQBurntDaily = new XYChart.Series<>(); - seriesBSQBurntDaily.setName(burntLabel); - - seriesBSQBurntDailyMA = new XYChart.Series<>(); - var burntMALabel = Res.get("dao.factsAndFigures.supply.burntMovingAverage"); - seriesBSQBurntDailyMA.setName(burntMALabel); - } - - private void createSupplyIncreasedVsDecreasedInformation() { - addTitledGroupBg(root, ++gridRow, 2, Res.get("dao.factsAndFigures.supply.issuedVsBurnt")); - - var chart = createBSQIssuedVsBurntChart(seriesBSQIssuedMonthly, seriesBSQBurntMonthly); - - var chartPane = wrapInChartPane(chart); - - addToTopMargin(chartPane); - - root.getChildren().add(chartPane); - } - - private void addToTopMargin(Node child) { - var margin = GridPane.getMargin(child); + private void createChart() { + addTitledGroupBg(root, gridRow, 2, Res.get("dao.factsAndFigures.supply.issuedVsBurnt")); + daoEconomyChartView.initialize(); - var new_insets = new Insets( - margin.getTop() + Layout.COMPACT_FIRST_ROW_DISTANCE, - margin.getRight(), - margin.getBottom(), - margin.getLeft() - ); + AnchorPane chartPane = new AnchorPane(); + chartPane.getStyleClass().add("chart-pane"); + VBox chartContainer = daoEconomyChartView.getRoot(); + AnchorPane.setTopAnchor(chartContainer, 15d); + AnchorPane.setBottomAnchor(chartContainer, 10d); + AnchorPane.setLeftAnchor(chartContainer, 25d); + AnchorPane.setRightAnchor(chartContainer, 10d); + GridPane.setColumnSpan(chartPane, 2); + GridPane.setRowIndex(chartPane, ++gridRow); + GridPane.setMargin(chartPane, new Insets(Layout.FIRST_ROW_DISTANCE, 0, 0, 0)); + chartPane.getChildren().add(chartContainer); - GridPane.setMargin(child, new_insets); + this.root.getChildren().add(chartPane); } - private void createSupplyIncreasedInformation() { - addTitledGroupBg(root, ++gridRow, 3, Res.get("dao.factsAndFigures.supply.issued"), Layout.GROUP_DISTANCE); + private void createIssuedAndBurnedFields() { + TitledGroupBg titledGroupBg = addTitledGroupBg(root, ++gridRow, 3, Res.get("dao.factsAndFigures.supply.issued"), Layout.GROUP_DISTANCE); + titledGroupBg.getStyleClass().add("last"); // hides separator as we add a second TitledGroupBg Tuple3 genesisAmountTuple = addTopLabelReadOnlyTextField(root, gridRow, Res.get("dao.factsAndFigures.supply.genesisIssueAmount"), Layout.FIRST_ROW_AND_GROUP_DISTANCE); @@ -251,26 +167,25 @@ private void createSupplyIncreasedInformation() { Res.get("dao.factsAndFigures.supply.compRequestIssueAmount")).second; reimbursementAmountTextField = addTopLabelReadOnlyTextField(root, gridRow, 1, Res.get("dao.factsAndFigures.supply.reimbursementAmount")).second; - } - private void createSupplyReducedInformation() { - addTitledGroupBg(root, ++gridRow, 3, Res.get("dao.factsAndFigures.supply.burnt"), Layout.GROUP_DISTANCE); + addTitledGroupBg(root, ++gridRow, 1, Res.get("dao.factsAndFigures.supply.burnt"), Layout.GROUP_DISTANCE_WITHOUT_SEPARATOR); - totalBurntTradeFeeTextField = addTopLabelReadOnlyTextField(root, gridRow, - Res.get("dao.factsAndFigures.supply.burntAmount"), Layout.FIRST_ROW_AND_GROUP_DISTANCE).second; - totalProofOfBurnAmountTextField = addTopLabelReadOnlyTextField(root, gridRow, 1, - Res.get("dao.factsAndFigures.supply.proofOfBurnAmount"), Layout.FIRST_ROW_AND_GROUP_DISTANCE).second; + totalBurntBsqTradeFeeTextField = addTopLabelReadOnlyTextField(root, gridRow, + Res.get("dao.factsAndFigures.supply.bsqTradeFee"), Layout.COMPACT_FIRST_ROW_AND_COMPACT_GROUP_DISTANCE).second; - var buttonTitle = Res.get("dao.factsAndFigures.supply.burntZoomToInliers"); - zoomToInliersSlide = addSlideToggleButton(root, ++gridRow, buttonTitle); + totalProofOfBurnAmountTextField = addTopLabelReadOnlyTextField(root, gridRow, 1, + Res.get("dao.factsAndFigures.supply.proofOfBurn"), Layout.COMPACT_FIRST_ROW_AND_COMPACT_GROUP_DISTANCE).second; - var chart = createBSQBurntChart(seriesBSQBurntDaily, seriesBSQBurntDailyMA); + /* totalBurntBtcTradeFeeTextField = addTopLabelReadOnlyTextField(root, gridRow, 1, + Res.get("dao.factsAndFigures.supply.btcTradeFee"), Layout.COMPACT_FIRST_ROW_AND_COMPACT_GROUP_DISTANCE).second; - var chartPane = wrapInChartPane(chart); - root.getChildren().add(chartPane); + Tuple3 tuple3 = addTopLabelReadOnlyTextField(root, ++gridRow, + Res.get("dao.factsAndFigures.supply.proofOfBurn")); + totalProofOfBurnAmountTextField = tuple3.second; + GridPane.setColumnSpan(tuple3.third, 2);*/ } - private void createSupplyLockedInformation() { + private void createLockedBsqFields() { TitledGroupBg titledGroupBg = addTitledGroupBg(root, ++gridRow, 2, Res.get("dao.factsAndFigures.supply.locked"), Layout.GROUP_DISTANCE); titledGroupBg.getStyleClass().add("last"); @@ -287,451 +202,51 @@ private void createSupplyLockedInformation() { Res.get("dao.factsAndFigures.supply.totalConfiscatedAmount")).second; } - // chart #1 (top) - private Node createBSQIssuedVsBurntChart( - XYChart.Series seriesBSQIssuedMonthly, - XYChart.Series seriesBSQBurntMonthly - ) { - xAxisChart1 = new NumberAxis(); - configureAxis(xAxisChart1); - xAxisChart1.setLabel("Month"); - xAxisChart1.setTickLabelFormatter(getMonthTickLabelFormatter("MM\nyyyy")); - addTickMarkLabelCssClass(xAxisChart1, "axis-tick-mark-text-node"); - - NumberAxis yAxis = new NumberAxis(); - configureYAxis(yAxis); - yAxis.setLabel("BSQ"); - yAxis.setTickLabelFormatter(BSQPriceTickLabelFormatter); - - var chart = new LineChart<>(xAxisChart1, yAxis); - configureChart(chart); - chart.setLegendVisible(true); - chart.setCreateSymbols(true); - - chart.getData().addAll(seriesBSQIssuedMonthly, seriesBSQBurntMonthly); - - return chart; - } - - // chart #3 (bottom) - private Node createBSQBurntChart( - XYChart.Series seriesBSQBurntDaily, - XYChart.Series seriesBSQBurntDailyMA - ) { - NumberAxis xAxis = new NumberAxis(); - configureAxis(xAxis); - xAxis.setTickLabelFormatter(getTimestampTickLabelFormatter("dd/MMM\nyyyy")); - addTickMarkLabelCssClass(xAxis, "axis-tick-mark-text-node"); - - NumberAxis yAxis = new NumberAxis(); - configureYAxis(yAxis); - yAxis.setLabel("BSQ"); - yAxis.setTickLabelFormatter(BSQPriceTickLabelFormatter); - - initializeChangeListener(yAxis); - - var chart = new LineChart<>(xAxis, yAxis); - configureChart(chart); - chart.setCreateSymbols(false); - chart.setLegendVisible(true); - - chart.getData().addAll(seriesBSQBurntDaily, seriesBSQBurntDailyMA); - - return chart; - } - - private void initializeChangeListener(NumberAxis axis) { - // Keep a class-scope reference. Needed for switching between inliers-only and full chart. - yAxisBSQBurntDaily = axis; - - changeListenerBSQBurntDaily = AxisInlierUtils.getListenerThatZoomsToInliers( - yAxisBSQBurntDaily, chartMaxNumberOfTicks, chartPercentToTrim, chartHowManyStdDevsConstituteOutlier); - } - public static List getListXMinMax(List> bsqList) { - long min = Long.MAX_VALUE, max = 0; - for (XYChart.Data data : bsqList) { - min = Math.min(data.getXValue().longValue(), min); - max = Math.max(data.getXValue().longValue(), max); - } - - return List.of(min, max); - } - - private void configureYAxis(NumberAxis axis) { - configureAxis(axis); - - axis.setForceZeroInRange(true); - axis.setSide(Side.RIGHT); - } - - private void configureAxis(NumberAxis axis) { - axis.setForceZeroInRange(false); - axis.setTickMarkVisible(true); - axis.setMinorTickVisible(false); - } - - // grab the axis tick mark label (text object) and add a CSS class. - private void addTickMarkLabelCssClass(NumberAxis axis, String cssClass) { - axis.getChildrenUnmodifiable().addListener((ListChangeListener) c -> { - while (c.next()) { - if (c.wasAdded()) { - for (Node mark : c.getAddedSubList()) { - if (mark instanceof Text) { - mark.getStyleClass().add(cssClass); - } - } - } - } - }); - } - - // rounds the tick timestamp to the nearest month - private StringConverter getMonthTickLabelFormatter(String datePattern) { - return new StringConverter<>() { - @Override - public String toString(Number timestamp) { - double tsd = timestamp.doubleValue(); - if ((chart1XBounds.size() == 2) && - ((tsd - monthDurationAvg / 2 < chart1XBounds.get(0).doubleValue()) || - (tsd + monthDurationAvg / 2 > chart1XBounds.get(1).doubleValue()))) { - return ""; - } - LocalDateTime localDateTime = LocalDateTime.ofEpochSecond(timestamp.longValue(), - 0, OffsetDateTime.now(ZoneId.systemDefault()).getOffset()); - if (localDateTime.getDayOfMonth() > 15) { - localDateTime = localDateTime.with(TemporalAdjusters.firstDayOfNextMonth()); - } - return localDateTime.format(DateTimeFormatter.ofPattern(datePattern, GlobalSettings.getLocale())); - } - - @Override - public Number fromString(String string) { - return 0; - } - }; - } - - private StringConverter getTimestampTickLabelFormatter(String datePattern) { - return new StringConverter<>() { - @Override - public String toString(Number timestamp) { - LocalDateTime localDateTime = LocalDateTime.ofEpochSecond(timestamp.longValue(), - 0, OffsetDateTime.now(ZoneId.systemDefault()).getOffset()); - return localDateTime.format(DateTimeFormatter.ofPattern(datePattern, GlobalSettings.getLocale())); - } - - @Override - public Number fromString(String string) { - return 0; - } - }; - } + /////////////////////////////////////////////////////////////////////////////////////////// + // Data + /////////////////////////////////////////////////////////////////////////////////////////// - private StringConverter BSQPriceTickLabelFormatter = - new StringConverter<>() { - @Override - public String toString(Number marketPrice) { - return dFmt.format(Double.parseDouble(bsqFormatter.formatBSQSatoshis(marketPrice.longValue()))); - } - - @Override - public Number fromString(String string) { - return 0; - } - }; - - private void configureChart(XYChart chart) { - chart.setAnimated(false); - chart.setId("charts-dao"); - - chart.setMinHeight(300); - chart.setPrefHeight(300); - chart.setPadding(new Insets(0)); + private void updateWithBsqBlockChainData() { + updateEconomicsData(); + updateLockedTxData(); } - private Pane wrapInChartPane(Node child) { - AnchorPane chartPane = new AnchorPane(); - chartPane.getStyleClass().add("chart-pane"); + private void updateEconomicsData() { + // We use the supplyDataProvider to get the adjusted data with static historical data as well to use the same + // monthly scoped data. + Coin issuedAmountFromCompRequests = Coin.valueOf(daoEconomyDataProvider.getCompensationAmount(fromDate, getToDate())); + compRequestIssueAmountTextField.setText(bsqFormatter.formatAmountWithGroupSeparatorAndCode(issuedAmountFromCompRequests)); - AnchorPane.setTopAnchor(child, 15d); - AnchorPane.setBottomAnchor(child, 10d); - AnchorPane.setLeftAnchor(child, 25d); - AnchorPane.setRightAnchor(child, 10d); + Coin issuedAmountFromReimbursementRequests = Coin.valueOf(daoEconomyDataProvider.getReimbursementAmount(fromDate, getToDate())); + reimbursementAmountTextField.setText(bsqFormatter.formatAmountWithGroupSeparatorAndCode(issuedAmountFromReimbursementRequests)); - chartPane.getChildren().add(child); + Coin totalBurntTradeFee = Coin.valueOf(daoEconomyDataProvider.getBsqTradeFeeAmount(fromDate, getToDate())); + totalBurntBsqTradeFeeTextField.setText(bsqFormatter.formatAmountWithGroupSeparatorAndCode(totalBurntTradeFee)); - GridPane.setColumnSpan(chartPane, 2); - GridPane.setRowIndex(chartPane, ++gridRow); - GridPane.setMargin(chartPane, new Insets(10, 0, 0, 0)); + /* Coin totalBurntBtcTradeFee = Coin.valueOf(daoEconomyDataProvider.getBtcTradeFeeAmount(fromDate, getToDate())); + totalBurntBtcTradeFeeTextField.setText(bsqFormatter.formatAmountWithGroupSeparatorAndCode(totalBurntBtcTradeFee)); +*/ + Coin totalProofOfBurnAmount = Coin.valueOf(daoEconomyDataProvider.getProofOfBurnAmount(fromDate, getToDate())); + totalProofOfBurnAmountTextField.setText(bsqFormatter.formatAmountWithGroupSeparatorAndCode(totalProofOfBurnAmount)); - return chartPane; } - private void updateWithBsqBlockChainData() { - Coin issuedAmountFromGenesis = daoFacade.getGenesisTotalSupply(); - genesisIssueAmountTextField.setText(bsqFormatter.formatAmountWithGroupSeparatorAndCode(issuedAmountFromGenesis)); - - Coin issuedAmountFromCompRequests = Coin.valueOf(daoFacade.getTotalIssuedAmount(IssuanceType.COMPENSATION)); - compRequestIssueAmountTextField.setText(bsqFormatter.formatAmountWithGroupSeparatorAndCode(issuedAmountFromCompRequests)); - Coin issuedAmountFromReimbursementRequests = Coin.valueOf(daoFacade.getTotalIssuedAmount(IssuanceType.REIMBURSEMENT)); - reimbursementAmountTextField.setText(bsqFormatter.formatAmountWithGroupSeparatorAndCode(issuedAmountFromReimbursementRequests)); - - Coin totalBurntTradeFee = Coin.valueOf(daoFacade.getTotalBurntTradeFee()); - Coin totalProofOfBurnAmount = Coin.valueOf(daoFacade.getTotalProofOfBurnAmount()); + private void updateLockedTxData() { Coin totalLockedUpAmount = Coin.valueOf(daoFacade.getTotalLockupAmount()); - Coin totalUnlockingAmount = Coin.valueOf(daoFacade.getTotalAmountOfUnLockingTxOutputs()); - Coin totalUnlockedAmount = Coin.valueOf(daoFacade.getTotalAmountOfUnLockedTxOutputs()); - Coin totalConfiscatedAmount = Coin.valueOf(daoFacade.getTotalAmountOfConfiscatedTxOutputs()); - - String minusSign = totalBurntTradeFee.isPositive() ? "-" : ""; - totalBurntTradeFeeTextField.setText(minusSign + bsqFormatter.formatAmountWithGroupSeparatorAndCode(totalBurntTradeFee)); - minusSign = totalProofOfBurnAmount.isPositive() ? "-" : ""; - totalProofOfBurnAmountTextField.setText(minusSign + bsqFormatter.formatAmountWithGroupSeparatorAndCode(totalProofOfBurnAmount)); totalLockedUpAmountTextField.setText(bsqFormatter.formatAmountWithGroupSeparatorAndCode(totalLockedUpAmount)); - totalUnlockingAmountTextField.setText(bsqFormatter.formatAmountWithGroupSeparatorAndCode(totalUnlockingAmount)); - totalUnlockedAmountTextField.setText(bsqFormatter.formatAmountWithGroupSeparatorAndCode(totalUnlockedAmount)); - totalConfiscatedAmountTextField.setText(bsqFormatter.formatAmountWithGroupSeparatorAndCode(totalConfiscatedAmount)); - - updateChartSeries(); - } - - private void updateChartSeries() { - var sortedBurntTxs = getSortedBurntTxs(); - - var updatedBurntBsqDaily = updateBSQBurntDaily(sortedBurntTxs); - updateBSQBurntDailyMA(updatedBurntBsqDaily); - - List xMinMaxB = updateBSQBurntMonthly(sortedBurntTxs); - - List xMinMaxI = updateBSQIssuedMonthly(); - - chart1XBounds = List.of(Math.min(xMinMaxB.get(0).doubleValue(), xMinMaxI.get(0).doubleValue()) - monthDurationAvg, - Math.max(xMinMaxB.get(1).doubleValue(), xMinMaxI.get(1).doubleValue()) + monthDurationAvg); - xAxisChart1.setAutoRanging(false); - xAxisChart1.setLowerBound(chart1XBounds.get(0).doubleValue()); - xAxisChart1.setUpperBound(chart1XBounds.get(1).doubleValue()); - xAxisChart1.setTickUnit(monthDurationAvg); - } - - private List getSortedBurntTxs() { - Set burntTxs = new HashSet<>(daoStateService.getBurntFeeTxs()); - burntTxs.addAll(daoStateService.getInvalidTxs()); - - return burntTxs.stream() - .sorted(Comparator.comparing(Tx::getTime)) - .collect(Collectors.toList()); - } - - private List> updateBSQBurntDaily(List sortedBurntTxs) { - seriesBSQBurntDaily.getData().clear(); - - var burntBsqByDay = - sortedBurntTxs - .stream() - .collect(Collectors.groupingBy( - tx -> Instant.ofEpochMilli(tx.getTime()).atZone(ZoneId.systemDefault()) - .toLocalDate() - .with(ADJUSTERS.get(DAY)) - )); - - List> updatedBurntBsqDaily = - burntBsqByDay - .keySet() - .stream() - .map(date -> { - ZonedDateTime zonedDateTime = date.atStartOfDay(ZoneId.systemDefault()); - return new XYChart.Data( - zonedDateTime.toInstant().getEpochSecond(), - burntBsqByDay.get(date) - .stream() - .mapToDouble(Tx::getBurntBsq) - .sum() - ); - }) - .collect(Collectors.toList()); - - seriesBSQBurntDaily.getData().setAll(updatedBurntBsqDaily); - - return updatedBurntBsqDaily; - } - - private List updateBSQBurntMonthly(List sortedBurntTxs) { - seriesBSQBurntMonthly.getData().clear(); - - var burntBsqByMonth = - sortedBurntTxs - .stream() - .collect(Collectors.groupingBy( - tx -> Instant.ofEpochMilli(tx.getTime()).atZone(ZoneId.systemDefault()) - .toLocalDate() - .with(ADJUSTERS.get(MONTH)) - )); - - List> updatedBurntBsqMonthly = - burntBsqByMonth - .keySet() - .stream() - .map(date -> { - ZonedDateTime zonedDateTime = date.atStartOfDay(ZoneId.systemDefault()); - return new XYChart.Data( - zonedDateTime.toInstant().getEpochSecond(), - burntBsqByMonth.get(date) - .stream() - .mapToDouble(Tx::getBurntBsq) - .sum() - ); - }) - .collect(Collectors.toList()); - - seriesBSQBurntMonthly.getData().setAll(updatedBurntBsqMonthly); - return getListXMinMax(updatedBurntBsqMonthly); - } - - private void updateBSQBurntDailyMA(List> updatedBurntBsq) { - seriesBSQBurntDailyMA.getData().clear(); - Comparator compareXChronology = - Comparator.comparingInt(Number::intValue); - - Comparator> compareXyDataChronology = - (xyData1, xyData2) -> - compareXChronology.compare( - xyData1.getXValue(), - xyData2.getXValue()); - - var sortedUpdatedBurntBsq = updatedBurntBsq - .stream() - .sorted(compareXyDataChronology) - .collect(Collectors.toList()); - - var burntBsqXValues = sortedUpdatedBurntBsq.stream().map(XYChart.Data::getXValue); - var burntBsqYValues = sortedUpdatedBurntBsq.stream().map(XYChart.Data::getYValue); - - var maPeriod = 15; - var burntBsqMAYValues = - MovingAverageUtils.simpleMovingAverage( - burntBsqYValues, - maPeriod); - - BiFunction> xyToXyData = - XYChart.Data::new; - - List> burntBsqMA = - zip(burntBsqXValues, burntBsqMAYValues, xyToXyData) - .filter(xyData -> Double.isFinite(xyData.getYValue().doubleValue())) - .collect(Collectors.toList()); - - seriesBSQBurntDailyMA.getData().setAll(burntBsqMA); - } - - private List updateBSQIssuedMonthly() { - Function blockTimeFn = memoize(height -> - Instant.ofEpochMilli(daoFacade.getBlockTime(height)).atZone(ZoneId.systemDefault()) - .toLocalDate() - .with(ADJUSTERS.get(MONTH))); - - Stream bsqByCompensation = daoStateService.getIssuanceSetForType(IssuanceType.COMPENSATION).stream() - .sorted(Comparator.comparing(Issuance::getChainHeight)); - - Stream bsqByReimbursement = daoStateService.getIssuanceSetForType(IssuanceType.REIMBURSEMENT).stream() - .sorted(Comparator.comparing(Issuance::getChainHeight)); - - Map> bsqAddedByVote = Stream.concat(bsqByCompensation, bsqByReimbursement) - .collect(Collectors.groupingBy(blockTimeFn.compose(Issuance::getChainHeight))); - - List> updatedAddedBSQ = bsqAddedByVote.keySet().stream() - .map(date -> { - ZonedDateTime zonedDateTime = date.atStartOfDay(ZoneId.systemDefault()); - return new XYChart.Data( - zonedDateTime.toInstant().getEpochSecond(), - bsqAddedByVote.get(date) - .stream() - .mapToDouble(Issuance::getAmount) - .sum()); - }) - .collect(Collectors.toList()); - - seriesBSQIssuedMonthly.getData().setAll(updatedAddedBSQ); - - return getListXMinMax(updatedAddedBSQ); - } - - private void activateButtons() { - zoomToInliersSlide.setSelected(isZoomingToInliers); - zoomToInliersSlide.setOnAction(e -> handleZoomToInliersSlide(!isZoomingToInliers)); - } - - private void deactivateButtons() { - zoomToInliersSlide.setOnAction(null); - } - - private void handleZoomToInliersSlide(boolean shouldActivate) { - isZoomingToInliers = !isZoomingToInliers; - if (shouldActivate) { - activateZoomingToInliers(); - } else { - deactivateZoomingToInliers(); - } - } - - private void activateZoomingToInliers() { - seriesBSQBurntDaily.getData().addListener(changeListenerBSQBurntDaily); - - // Initial zoom has to be triggered manually; otherwise, it - // would be triggered only on a change event in the series - triggerZoomToInliers(); - } - - private void deactivateZoomingToInliers() { - seriesBSQBurntDaily.getData().removeListener(changeListenerBSQBurntDaily); - - // Reactivate automatic ranging - yAxisBSQBurntDaily.autoRangingProperty().set(true); - } + Coin totalUnlockingAmount = Coin.valueOf(daoFacade.getTotalAmountOfUnLockingTxOutputs()); + totalUnlockingAmountTextField.setText(bsqFormatter.formatAmountWithGroupSeparatorAndCode(totalUnlockingAmount)); - private void triggerZoomToInliers() { - var xyValues = seriesBSQBurntDaily.getData(); - AxisInlierUtils.zoomToInliers( - yAxisBSQBurntDaily, - xyValues, - chartMaxNumberOfTicks, - chartPercentToTrim, - chartHowManyStdDevsConstituteOutlier - ); - } + Coin totalUnlockedAmount = Coin.valueOf(daoFacade.getTotalAmountOfUnLockedTxOutputs()); + totalUnlockedAmountTextField.setText(bsqFormatter.formatAmountWithGroupSeparatorAndCode(totalUnlockedAmount)); - // When Guava version is bumped to at least 21.0, - // can be replaced with com.google.common.collect.Streams.zip - private static Stream zip( - Stream leftStream, - Stream rightStream, - BiFunction combiner - ) { - var lefts = leftStream.spliterator(); - var rights = rightStream.spliterator(); - var spliterator = - new AbstractSpliterator( - Long.min( - lefts.estimateSize(), - rights.estimateSize() - ), - lefts.characteristics() & rights.characteristics() - ) { - @Override - public boolean tryAdvance(Consumer action) { - return lefts.tryAdvance( - left -> rights.tryAdvance( - right -> action.accept(combiner.apply(left, right)) - ) - ); - } - }; - return StreamSupport.stream(spliterator, false); + Coin totalConfiscatedAmount = Coin.valueOf(daoFacade.getTotalAmountOfConfiscatedTxOutputs()); + totalConfiscatedAmountTextField.setText(bsqFormatter.formatAmountWithGroupSeparatorAndCode(totalConfiscatedAmount)); } - private static Function memoize(Function fn) { - Map map = new ConcurrentHashMap<>(); - return x -> map.computeIfAbsent(x, fn); + private long getToDate() { + return toDate > 0 ? toDate : System.currentTimeMillis(); } } diff --git a/desktop/src/main/java/bisq/desktop/main/dao/economy/supply/chart/DaoEconomyChartModel.java b/desktop/src/main/java/bisq/desktop/main/dao/economy/supply/chart/DaoEconomyChartModel.java new file mode 100644 index 00000000000..b6f7e160fec --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/main/dao/economy/supply/chart/DaoEconomyChartModel.java @@ -0,0 +1,103 @@ +/* + * This file is part of Bisq. + * + * Bisq is free software: you can redistribute it and/or modify it + * under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at + * your option) any later version. + * + * Bisq is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public + * License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Bisq. If not, see . + */ + +package bisq.desktop.main.dao.economy.supply.chart; + +import bisq.desktop.components.chart.ChartModel; +import bisq.desktop.main.dao.economy.supply.DaoEconomyDataProvider; + +import bisq.core.dao.state.DaoStateService; + +import bisq.common.util.Tuple2; + +import javax.inject.Inject; + +import javafx.scene.chart.XYChart; + +import java.util.List; +import java.util.Map; +import java.util.function.Predicate; +import java.util.stream.Collectors; + +import lombok.extern.slf4j.Slf4j; + +@Slf4j +public class DaoEconomyChartModel extends ChartModel { + private final DaoStateService daoStateService; + private final DaoEconomyDataProvider daoEconomyDataProvider; + + /////////////////////////////////////////////////////////////////////////////////////////// + // Constructor + /////////////////////////////////////////////////////////////////////////////////////////// + + @Inject + public DaoEconomyChartModel(DaoStateService daoStateService, DaoEconomyDataProvider daoEconomyDataProvider) { + super(); + this.daoStateService = daoStateService; + + this.daoEconomyDataProvider = daoEconomyDataProvider; + } + + List> getBsqTradeFeeChartData(Predicate predicate) { + return toChartData(daoEconomyDataProvider.getBurnedBsqByMonth(daoStateService.getTradeFeeTxs(), predicate)); + } + + // The resulting data are not very useful. It causes negative values if burn rate > issuance in selected timeframe + /* List> getBtcTradeFeeChartData(Predicate predicate) { + return toChartData(daoEconomyDataProvider.getBurnedBtcByMonth(predicate)); + }*/ + + List> getCompensationChartData(Predicate predicate) { + return toChartData(daoEconomyDataProvider.getMergedCompensationMap(predicate)); + } + + List> getProofOfBurnChartData(Predicate predicate) { + return toChartData(daoEconomyDataProvider.getBurnedBsqByMonth(daoStateService.getProofOfBurnTxs(), predicate)); + } + + List> getReimbursementChartData(Predicate predicate) { + return toChartData(daoEconomyDataProvider.getMergedReimbursementMap(predicate)); + } + + void initBounds(List> tradeFeeChartData, + List> compensationRequestsChartData) { + Tuple2 xMinMaxTradeFee = getMinMax(tradeFeeChartData); + Tuple2 xMinMaxCompensationRequest = getMinMax(compensationRequestsChartData); + + lowerBound = Math.min(xMinMaxTradeFee.first, xMinMaxCompensationRequest.first); + upperBound = Math.max(xMinMaxTradeFee.second, xMinMaxCompensationRequest.second); + } + + /////////////////////////////////////////////////////////////////////////////////////////// + // Utils + /////////////////////////////////////////////////////////////////////////////////////////// + + private static List> toChartData(Map map) { + return map.entrySet().stream() + .map(entry -> new XYChart.Data(entry.getKey(), entry.getValue())) + .collect(Collectors.toList()); + } + + private static Tuple2 getMinMax(List> chartData) { + long min = Long.MAX_VALUE, max = 0; + for (XYChart.Data data : chartData) { + min = Math.min(data.getXValue().longValue(), min); + max = Math.max(data.getXValue().longValue(), max); + } + return new Tuple2<>((double) min, (double) max); + } +} diff --git a/desktop/src/main/java/bisq/desktop/main/dao/economy/supply/chart/DaoEconomyChartView.java b/desktop/src/main/java/bisq/desktop/main/dao/economy/supply/chart/DaoEconomyChartView.java new file mode 100644 index 00000000000..2ae739b4c7e --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/main/dao/economy/supply/chart/DaoEconomyChartView.java @@ -0,0 +1,265 @@ +/* + * This file is part of Bisq. + * + * Bisq is free software: you can redistribute it and/or modify it + * under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at + * your option) any later version. + * + * Bisq is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public + * License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Bisq. If not, see . + */ + +package bisq.desktop.main.dao.economy.supply.chart; + +import bisq.desktop.components.chart.ChartView; +import bisq.desktop.main.dao.economy.supply.DaoEconomyDataProvider; +import bisq.desktop.util.DisplayUtils; + +import bisq.core.locale.Res; +import bisq.core.util.coin.BsqFormatter; + +import bisq.common.UserThread; + +import javax.inject.Inject; + +import javafx.scene.Node; +import javafx.scene.chart.LineChart; +import javafx.scene.chart.NumberAxis; +import javafx.scene.chart.XYChart; +import javafx.scene.control.Tooltip; +import javafx.scene.text.Text; + +import javafx.geometry.Side; + +import javafx.collections.ListChangeListener; + +import javafx.util.StringConverter; + +import java.time.Instant; + +import java.text.DecimalFormat; + +import java.util.Date; +import java.util.List; +import java.util.function.Predicate; + +import lombok.extern.slf4j.Slf4j; + +@Slf4j +public class DaoEconomyChartView extends ChartView { + private static final DecimalFormat priceFormat = new DecimalFormat(",###"); + private final BsqFormatter bsqFormatter; + + private XYChart.Series seriesBsqTradeFee, seriesProofOfBurn, seriesCompensation, + seriesReimbursement/*, seriesBtcTradeFee*/; + private ListChangeListener nodeListChangeListener; + + @Inject + public DaoEconomyChartView(DaoEconomyChartModel model, BsqFormatter bsqFormatter) { + super(model); + + this.bsqFormatter = bsqFormatter; + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Lifecycle + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public void initialize() { + super.initialize(); + + // Turn off some series + hideSeries(seriesProofOfBurn); + hideSeries(seriesReimbursement); + /* hideSeries(seriesBtcTradeFee);*/ + + nodeListChangeListener = c -> { + while (c.next()) { + if (c.wasAdded()) { + for (Node mark : c.getAddedSubList()) { + if (mark instanceof Text) { + mark.getStyleClass().add("axis-tick-mark-text-node"); + } + } + } + } + }; + } + + @Override + public void activate() { + super.activate(); + xAxis.getChildrenUnmodifiable().addListener(nodeListChangeListener); + } + + @Override + public void deactivate() { + super.deactivate(); + xAxis.getChildrenUnmodifiable().removeListener(nodeListChangeListener); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Customisations + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + protected NumberAxis getXAxis() { + NumberAxis xAxis = new NumberAxis(); + xAxis.setForceZeroInRange(false); + xAxis.setTickLabelFormatter(getTimeAxisStringConverter()); + return xAxis; + } + + @Override + protected NumberAxis getYAxis() { + NumberAxis yAxis = new NumberAxis(); + yAxis.setForceZeroInRange(false); + yAxis.setSide(Side.RIGHT); + yAxis.setTickLabelFormatter(getYAxisStringConverter()); + return yAxis; + } + + @Override + protected XYChart getChart() { + LineChart chart = new LineChart<>(xAxis, yAxis); + chart.setAnimated(false); + chart.setCreateSymbols(true); + chart.setLegendVisible(false); + chart.setId("charts-dao"); + return chart; + } + + @Override + protected StringConverter getTimeAxisStringConverter() { + return new StringConverter<>() { + @Override + public String toString(Number epochSeconds) { + Date date = new Date(DaoEconomyDataProvider.toStartOfMonth(Instant.ofEpochSecond(epochSeconds.longValue())) * 1000); + return DisplayUtils.formatDateAxis(date, "dd/MMM\nyyyy"); + } + + @Override + public Number fromString(String string) { + return 0; + } + }; + } + + @Override + protected StringConverter getYAxisStringConverter() { + return new StringConverter<>() { + @Override + public String toString(Number value) { + return priceFormat.format(Double.parseDouble(bsqFormatter.formatBSQSatoshis(value.longValue()))) + " BSQ"; + } + + @Override + public Number fromString(String string) { + return null; + } + }; + } + + @Override + protected void addSeries() { + seriesBsqTradeFee = new XYChart.Series<>(); + seriesBsqTradeFee.setName(Res.get("dao.factsAndFigures.supply.bsqTradeFee")); + seriesIndexMap.put(seriesBsqTradeFee.getName(), 0); + chart.getData().add(seriesBsqTradeFee); + + /* seriesBtcTradeFee = new XYChart.Series<>(); + seriesBtcTradeFee.setName(Res.get("dao.factsAndFigures.supply.btcTradeFee")); + seriesIndexMap.put(seriesBtcTradeFee.getName(), 4); + chart.getData().add(seriesBtcTradeFee);*/ + + seriesCompensation = new XYChart.Series<>(); + seriesCompensation.setName(Res.get("dao.factsAndFigures.supply.compReq")); + seriesIndexMap.put(seriesCompensation.getName(), 1); + chart.getData().add(seriesCompensation); + + seriesProofOfBurn = new XYChart.Series<>(); + seriesProofOfBurn.setName(Res.get("dao.factsAndFigures.supply.proofOfBurn")); + seriesIndexMap.put(seriesProofOfBurn.getName(), 2); + chart.getData().add(seriesProofOfBurn); + + seriesReimbursement = new XYChart.Series<>(); + seriesReimbursement.setName(Res.get("dao.factsAndFigures.supply.reimbursement")); + seriesIndexMap.put(seriesReimbursement.getName(), 3); + chart.getData().add(seriesReimbursement); + } + + @Override + protected void initData() { + List> bsqTradeFeeChartData = model.getBsqTradeFeeChartData(e -> true); + seriesBsqTradeFee.getData().setAll(bsqTradeFeeChartData); + + /* List> btcTradeFeeChartData = model.getBtcTradeFeeChartData(e -> true); + seriesBtcTradeFee.getData().setAll(btcTradeFeeChartData);*/ + + List> compensationRequestsChartData = model.getCompensationChartData(e -> true); + seriesCompensation.getData().setAll(compensationRequestsChartData); + + List> proofOfBurnChartData = model.getProofOfBurnChartData(e -> true); + seriesProofOfBurn.getData().setAll(proofOfBurnChartData); + + List> reimbursementChartData = model.getReimbursementChartData(e -> true); + seriesReimbursement.getData().setAll(reimbursementChartData); + + applyTooltip(); + + // We don't need redundant data like reimbursementChartData as time value from compensationRequestsChartData + // will cover it + model.initBounds(bsqTradeFeeChartData, compensationRequestsChartData); + xAxis.setLowerBound(model.getLowerBound().doubleValue()); + xAxis.setUpperBound(model.getUpperBound().doubleValue()); + + UserThread.execute(this::setTimeLineLabels); + } + + @Override + protected void updateData(Predicate predicate) { + List> tradeFeeChartData = model.getBsqTradeFeeChartData(predicate); + seriesBsqTradeFee.getData().setAll(tradeFeeChartData); + + /* List> btcTradeFeeChartData = model.getBtcTradeFeeChartData(predicate); + seriesBtcTradeFee.getData().setAll(btcTradeFeeChartData);*/ + + List> compensationRequestsChartData = model.getCompensationChartData(predicate); + seriesCompensation.getData().setAll(compensationRequestsChartData); + + List> proofOfBurnChartData = model.getProofOfBurnChartData(predicate); + seriesProofOfBurn.getData().setAll(proofOfBurnChartData); + + List> reimbursementChartData = model.getReimbursementChartData(predicate); + seriesReimbursement.getData().setAll(reimbursementChartData); + + applyTooltip(); + } + + @Override + protected void applyTooltip() { + chart.getData().forEach(series -> { + String format = series == seriesCompensation || series == seriesReimbursement ? + "dd MMM yyyy" : + "MMM yyyy"; + series.getData().forEach(data -> { + String xValue = DisplayUtils.formatDateAxis(new Date(data.getXValue().longValue() * 1000), format); + String yValue = bsqFormatter.formatBSQSatoshisWithCode(data.getYValue().longValue()); + Node node = data.getNode(); + if (node == null) { + return; + } + Tooltip.install(node, new Tooltip(Res.get("dao.factsAndFigures.supply.chart.tradeFee.toolTip", yValue, xValue))); + }); + }); + } +} diff --git a/desktop/src/main/java/bisq/desktop/theme-dark.css b/desktop/src/main/java/bisq/desktop/theme-dark.css index 612f29d0eeb..39d296b62c8 100644 --- a/desktop/src/main/java/bisq/desktop/theme-dark.css +++ b/desktop/src/main/java/bisq/desktop/theme-dark.css @@ -139,6 +139,9 @@ /* dao chart colors */ -bs-chart-dao-line1: -bs-color-green-5; -bs-chart-dao-line2: -bs-color-blue-2; + -bs-chart-dao-line3: -bs-turquoise; + -bs-chart-dao-line4: -bs-yellow; + -bs-chart-dao-line5: -bs-color-blue-0; /* Monero orange color code */ -xmr-orange: #f26822; diff --git a/desktop/src/main/java/bisq/desktop/theme-light.css b/desktop/src/main/java/bisq/desktop/theme-light.css index 76c268cdf58..c296614e295 100644 --- a/desktop/src/main/java/bisq/desktop/theme-light.css +++ b/desktop/src/main/java/bisq/desktop/theme-light.css @@ -106,6 +106,9 @@ /* dao chart colors */ -bs-chart-dao-line1: -bs-color-green-3; -bs-chart-dao-line2: -bs-color-blue-5; + -bs-chart-dao-line3: -bs-turquoise; + -bs-chart-dao-line4: -bs-yellow; + -bs-chart-dao-line5: -bs-color-blue-0; /* Monero orange color code */ -xmr-orange: #f26822; diff --git a/desktop/src/main/java/bisq/desktop/util/FormBuilder.java b/desktop/src/main/java/bisq/desktop/util/FormBuilder.java index 94013d6acab..b0217cdeb19 100644 --- a/desktop/src/main/java/bisq/desktop/util/FormBuilder.java +++ b/desktop/src/main/java/bisq/desktop/util/FormBuilder.java @@ -76,6 +76,7 @@ import javafx.scene.layout.HBox; import javafx.scene.layout.Pane; import javafx.scene.layout.Priority; +import javafx.scene.layout.Region; import javafx.scene.layout.VBox; import javafx.scene.text.Text; @@ -607,11 +608,49 @@ public static Tuple2 addTopLabelDatePicker(GridPane gridPane, int rowIndex, String title, double top) { + return addTopLabelDatePicker(gridPane, rowIndex, 0, title, top); + } + + public static Tuple2 addTopLabelDatePicker(GridPane gridPane, + int rowIndex, + int columnIndex, + String title, + double top) { DatePicker datePicker = new JFXDatePicker(); + Tuple2 topLabelWithVBox = addTopLabelWithVBox(gridPane, rowIndex, columnIndex, title, datePicker, top); + return new Tuple2<>(topLabelWithVBox.first, datePicker); + } + + /////////////////////////////////////////////////////////////////////////////////////////// + // 2 DatePickers + /////////////////////////////////////////////////////////////////////////////////////////// - final Tuple2 topLabelWithVBox = addTopLabelWithVBox(gridPane, rowIndex, title, datePicker, top); + public static Tuple2 add2TopLabelDatePicker(GridPane gridPane, + int rowIndex, + int columnIndex, + String title1, + String title2, + double top) { + DatePicker datePicker1 = new JFXDatePicker(); + Tuple2 topLabelWithVBox1 = getTopLabelWithVBox(title1, datePicker1); + VBox vBox1 = topLabelWithVBox1.second; - return new Tuple2<>(topLabelWithVBox.first, datePicker); + DatePicker datePicker2 = new JFXDatePicker(); + Tuple2 topLabelWithVBox2 = getTopLabelWithVBox(title2, datePicker2); + VBox vBox2 = topLabelWithVBox2.second; + + Region spacer = new Region(); + HBox.setHgrow(spacer, Priority.ALWAYS); + + HBox hBox = new HBox(); + hBox.setSpacing(10); + hBox.getChildren().addAll(spacer, vBox1, vBox2); + + GridPane.setRowIndex(hBox, rowIndex); + GridPane.setColumnIndex(hBox, columnIndex); + GridPane.setMargin(hBox, new Insets(top, 0, 0, 0)); + gridPane.getChildren().add(hBox); + return new Tuple2<>(datePicker1, datePicker2); } @@ -906,10 +945,10 @@ public static Tuple2 addButtonCheckBox(GridPane gridPane, } public static Tuple3 addButtonCheckBoxWithBox(GridPane gridPane, - int rowIndex, - String buttonTitle, - String checkBoxTitle, - double top) { + int rowIndex, + String buttonTitle, + String checkBoxTitle, + double top) { Button button = new AutoTooltipButton(buttonTitle); CheckBox checkBox = new AutoTooltipCheckBox(checkBoxTitle); From 7c82271ac17c82a1aeb2dc74c43e35c2f4b9679c Mon Sep 17 00:00:00 2001 From: chimp1984 Date: Fri, 5 Feb 2021 12:47:14 -0500 Subject: [PATCH 05/21] Fix data (was missing the reimbursement to the RA due a mistake by doing a double payout). we consider that as reimbursement as well. --- .../desktop/main/dao/economy/supply/DaoEconomyDataProvider.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/desktop/src/main/java/bisq/desktop/main/dao/economy/supply/DaoEconomyDataProvider.java b/desktop/src/main/java/bisq/desktop/main/dao/economy/supply/DaoEconomyDataProvider.java index 69efddfec2f..e23068585c7 100644 --- a/desktop/src/main/java/bisq/desktop/main/dao/economy/supply/DaoEconomyDataProvider.java +++ b/desktop/src/main/java/bisq/desktop/main/dao/economy/supply/DaoEconomyDataProvider.java @@ -227,7 +227,7 @@ private static class DaoEconomyHistoricalData { REIMBURSEMENTS_BY_CYCLE_DATE.put(1585342220L, 12089400L); REIMBURSEMENTS_BY_CYCLE_DATE.put(1588025030L, 5420700L); REIMBURSEMENTS_BY_CYCLE_DATE.put(1591004931L, 9138760L); - REIMBURSEMENTS_BY_CYCLE_DATE.put(1593654027L, 7407637L); + REIMBURSEMENTS_BY_CYCLE_DATE.put(1593654027L, 10821807L); REIMBURSEMENTS_BY_CYCLE_DATE.put(1596407074L, 2160157L); REIMBURSEMENTS_BY_CYCLE_DATE.put(1599175867L, 8769408L); REIMBURSEMENTS_BY_CYCLE_DATE.put(1601861442L, 4956585L); From 9dab4fd65a88960c943546c029169238c692866f Mon Sep 17 00:00:00 2001 From: chimp1984 Date: Fri, 5 Feb 2021 15:48:44 -0500 Subject: [PATCH 06/21] Add total issuance and total burned series --- .../resources/i18n/displayStrings.properties | 2 + desktop/src/main/java/bisq/desktop/bisq.css | 12 ++- .../desktop/components/chart/ChartView.java | 60 +++++++----- .../supply/DaoEconomyDataProvider.java | 59 ++++-------- .../main/dao/economy/supply/SupplyView.java | 15 +-- .../supply/chart/DaoEconomyChartModel.java | 19 +++- .../supply/chart/DaoEconomyChartView.java | 96 ++++++++++--------- .../src/main/java/bisq/desktop/theme-dark.css | 5 +- .../main/java/bisq/desktop/theme-light.css | 5 +- 9 files changed, 141 insertions(+), 132 deletions(-) diff --git a/core/src/main/resources/i18n/displayStrings.properties b/core/src/main/resources/i18n/displayStrings.properties index e71ea804b6a..82210faefae 100644 --- a/core/src/main/resources/i18n/displayStrings.properties +++ b/core/src/main/resources/i18n/displayStrings.properties @@ -2443,6 +2443,8 @@ dao.factsAndFigures.supply.reimbursement=Reimbursement requests dao.factsAndFigures.supply.genesisIssueAmount=BSQ issued at genesis transaction dao.factsAndFigures.supply.compRequestIssueAmount=BSQ issued for compensation requests dao.factsAndFigures.supply.reimbursementAmount=BSQ issued for reimbursement requests +dao.factsAndFigures.supply.totalIssued=Total issues BSQ +dao.factsAndFigures.supply.totalBurned=Total burned BSQ dao.factsAndFigures.supply.chart.tradeFee.toolTip={0}\n{1} diff --git a/desktop/src/main/java/bisq/desktop/bisq.css b/desktop/src/main/java/bisq/desktop/bisq.css index 9e16c8a270b..17a8ba8b468 100644 --- a/desktop/src/main/java/bisq/desktop/bisq.css +++ b/desktop/src/main/java/bisq/desktop/bisq.css @@ -1744,6 +1744,9 @@ textfield */ #charts-dao .default-color4.chart-series-line { -fx-stroke: -bs-chart-dao-line5; } #charts-dao .default-color4.chart-line-symbol { -fx-background-color: -bs-chart-dao-line5, -bs-chart-dao-line5; } +#charts-dao .default-color5.chart-series-line { -fx-stroke: -bs-chart-dao-line6; } +#charts-dao .default-color5.chart-line-symbol { -fx-background-color: -bs-chart-dao-line6, -bs-chart-dao-line6; } + #charts-legend-toggle0 { -jfx-toggle-color: -bs-chart-dao-line1 } @@ -1759,6 +1762,9 @@ textfield */ #charts-legend-toggle4 { -jfx-toggle-color: -bs-chart-dao-line5; } +#charts-legend-toggle5 { + -jfx-toggle-color: -bs-chart-dao-line6; +} #charts-dao .chart-series-line { -fx-stroke-width: 2px; @@ -1785,11 +1791,15 @@ textfield */ } #chart-navigation-label { - -fx-tick-label-fill: -bs-rd-font-lighter; + -fx-text-fill: -bs-rd-font-lighter; -fx-font-size: 0.769em; -fx-alignment: center; } +#chart-navigation-center-pane { + -fx-background-color: -bs-progress-bar-track; +} + /******************************************************************************************************************** * * * Highlight buttons * diff --git a/desktop/src/main/java/bisq/desktop/components/chart/ChartView.java b/desktop/src/main/java/bisq/desktop/components/chart/ChartView.java index 6fa3848940f..b7d2dd988d4 100644 --- a/desktop/src/main/java/bisq/desktop/components/chart/ChartView.java +++ b/desktop/src/main/java/bisq/desktop/components/chart/ChartView.java @@ -38,6 +38,7 @@ import javafx.scene.layout.VBox; import javafx.geometry.Insets; +import javafx.geometry.Pos; import javafx.beans.value.ChangeListener; @@ -47,6 +48,7 @@ import java.text.SimpleDateFormat; import java.util.ArrayList; +import java.util.Collection; import java.util.Date; import java.util.HashMap; import java.util.HashSet; @@ -70,7 +72,6 @@ public abstract class ChartView extends ActivatableView dividerNodes = new ArrayList<>(); private final Double[] dividerPositions = new Double[]{0d, 1d}; - private HBox legendBox; private boolean pressed; private double x; private ChangeListener widthListener; @@ -91,25 +92,35 @@ public ChartView(T model) { chart = getChart(); - addSeries(); - addLegend(); - timelineLabels = new HBox(); - VBox box = new VBox(); - int paddingRight = 60; - VBox.setMargin(splitPane, new Insets(20, paddingRight, 0, 0)); - VBox.setMargin(timelineLabels, new Insets(0, paddingRight, 0, 0)); - VBox.setMargin(legendBox, new Insets(10, paddingRight, 0, 0)); - box.getChildren().addAll(splitPane, timelineLabels, legendBox); + HBox legendBox1 = getLegendBox(getSeriesForLegend1()); + Collection> seriesForLegend2 = getSeriesForLegend2(); + HBox legendBox2 = null; + if (seriesForLegend2 != null && !seriesForLegend2.isEmpty()) { + legendBox2 = getLegendBox(seriesForLegend2); + } - VBox vBox = new VBox(); - vBox.setSpacing(10); - vBox.getChildren().addAll(chart, box); + timelineLabels = new HBox(); + VBox box = new VBox(); + int paddingRight = 89; + int paddingLeft = 15; + VBox.setMargin(splitPane, new Insets(0, paddingRight, 0, paddingLeft)); + VBox.setMargin(timelineLabels, new Insets(0, paddingRight, 0, paddingLeft)); + VBox.setMargin(legendBox1, new Insets(10, paddingRight, 0, paddingLeft)); + box.getChildren().addAll(splitPane, timelineLabels, legendBox1); + if (legendBox2 != null) { + VBox.setMargin(legendBox2, new Insets(-20, paddingRight, 0, paddingLeft)); + box.getChildren().add(legendBox2); + } root.getChildren().addAll(chart, box); } + protected abstract Collection> getSeriesForLegend1(); + + protected abstract Collection> getSeriesForLegend2(); + /////////////////////////////////////////////////////////////////////////////////////////// // Lifecycle @@ -117,7 +128,7 @@ public ChartView(T model) { @Override public void initialize() { - center.setStyle("-fx-background-color: #dddddd"); + center.setId("chart-navigation-center-pane"); splitPane.setMinHeight(30); splitPane.setDividerPosition(0, dividerPositions[0]); @@ -191,26 +202,25 @@ protected XYChart getChart() { protected abstract void addSeries(); - protected void addLegend() { - legendBox = new HBox(); - legendBox.setSpacing(10); - Region spacer = new Region(); - HBox.setHgrow(spacer, Priority.ALWAYS); - legendBox.getChildren().add(spacer); - - chart.getData().forEach(series -> { + protected HBox getLegendBox(Collection> data) { + HBox hBox = new HBox(); + hBox.setSpacing(10); + data.forEach(series -> { AutoTooltipSlideToggleButton toggle = new AutoTooltipSlideToggleButton(); + toggle.setMinWidth(200); + toggle.setAlignment(Pos.TOP_LEFT); String seriesName = series.getName(); toggleBySeriesName.put(seriesName, toggle); toggle.setText(seriesName); toggle.setId("charts-legend-toggle" + seriesIndexMap.get(seriesName)); toggle.setSelected(true); toggle.setOnAction(e -> onSelectToggle(series, toggle.isSelected())); - legendBox.getChildren().add(toggle); + hBox.getChildren().add(toggle); }); - spacer = new Region(); + Region spacer = new Region(); HBox.setHgrow(spacer, Priority.ALWAYS); - legendBox.getChildren().add(spacer); + hBox.getChildren().add(spacer); + return hBox; } private void onSelectToggle(XYChart.Series series, boolean isSelected) { diff --git a/desktop/src/main/java/bisq/desktop/main/dao/economy/supply/DaoEconomyDataProvider.java b/desktop/src/main/java/bisq/desktop/main/dao/economy/supply/DaoEconomyDataProvider.java index e23068585c7..92ed0a1855b 100644 --- a/desktop/src/main/java/bisq/desktop/main/dao/economy/supply/DaoEconomyDataProvider.java +++ b/desktop/src/main/java/bisq/desktop/main/dao/economy/supply/DaoEconomyDataProvider.java @@ -35,6 +35,7 @@ import java.util.Map; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; +import java.util.function.BinaryOperator; import java.util.function.Function; import java.util.function.Predicate; import java.util.stream.Collectors; @@ -96,17 +97,6 @@ public long getBsqTradeFeeAmount(long fromDate, long toDate) { .sum(); } - /** - * @param fromDate Epoch in millis - * @param toDate Epoch in millis - */ - /* public long getBtcTradeFeeAmount(long fromDate, long toDate) { - return getBurnedBtcByMonth(getPredicate(fromDate, toDate)).values() - .stream() - .mapToLong(e -> e) - .sum(); - }*/ - /** * @param fromDate Epoch in millis * @param toDate Epoch in millis @@ -147,49 +137,38 @@ public Map getIssuedBsqByMonth(Set issuanceSet, Predicate< } public Map getMergedCompensationMap(Predicate predicate) { - return getMergedMap(daoStateService.getIssuanceSetForType(IssuanceType.COMPENSATION), - DaoEconomyHistoricalData.COMPENSATIONS_BY_CYCLE_DATE, - predicate); + Map issuedBsqByMonth = getIssuedBsqByMonth(daoStateService.getIssuanceSetForType(IssuanceType.COMPENSATION), predicate); + Map historicalIssuanceByMonth = getHistoricalIssuanceByMonth(DaoEconomyHistoricalData.COMPENSATIONS_BY_CYCLE_DATE, predicate); + return getMergedMap(issuedBsqByMonth, historicalIssuanceByMonth, (daoDataValue, staticDataValue) -> staticDataValue); } public Map getMergedReimbursementMap(Predicate predicate) { - return getMergedMap(daoStateService.getIssuanceSetForType(IssuanceType.REIMBURSEMENT), - DaoEconomyHistoricalData.REIMBURSEMENTS_BY_CYCLE_DATE, - predicate); + Map issuedBsqByMonth = getIssuedBsqByMonth(daoStateService.getIssuanceSetForType(IssuanceType.REIMBURSEMENT), predicate); + Map historicalIssuanceByMonth = getHistoricalIssuanceByMonth(DaoEconomyHistoricalData.REIMBURSEMENTS_BY_CYCLE_DATE, predicate); + return getMergedMap(issuedBsqByMonth, historicalIssuanceByMonth, (daoDataValue, staticDataValue) -> staticDataValue); + } + + public Map getMergedMap(Map map1, + Map map2, + BinaryOperator mergeFunction) { + return Stream.concat(map1.entrySet().stream(), + map2.entrySet().stream()) + .collect(Collectors.toMap(Map.Entry::getKey, + Map.Entry::getValue, + mergeFunction)); } - private Map getMergedMap(Set issuanceSet, - Map historicalData, - Predicate predicate) { + private Map getHistoricalIssuanceByMonth(Map historicalData, Predicate predicate) { // We did not use the reimbursement requests initially (but the compensation requests) because the limits // have been too low. Over time it got mixed in compensation requests and reimbursement requests. // To reflect that we use static data derived from the Github data. For new data we do not need that anymore // as we have clearly separated that now. In case we have duplicate data for a months we use the static data. - Map historicalDataMap = historicalData.entrySet().stream() + return historicalData.entrySet().stream() .filter(e -> predicate.test(e.getKey())) .collect(Collectors.toMap(e -> toStartOfMonth(Instant.ofEpochSecond(e.getKey())), Map.Entry::getValue)); - - // We merge both maps. - // If we have 2 values at same key we use the staticData as that include the daoData - return Stream.concat(getIssuedBsqByMonth(issuanceSet, predicate).entrySet().stream(), - historicalDataMap.entrySet().stream()) - .collect(Collectors.toMap(Map.Entry::getKey, - Map.Entry::getValue, - (issuedBsqByMonthValue, staticDataValue) -> staticDataValue)); } - // The resulting data are not very useful. We might drop that.... - /* public Map getBurnedBtcByMonth(Predicate predicate) { - Map issuedBsqByMonth = getMergedReimbursementMap(predicate); - Map burnedBsqByMonth = getBurnedBsqByMonth(daoStateService.getProofOfBurnTxs(), predicate); - return Stream.concat(issuedBsqByMonth.entrySet().stream(), - burnedBsqByMonth.entrySet().stream()) - .collect(Collectors.toMap(Map.Entry::getKey, - Map.Entry::getValue, - (issued, burned) -> burned - issued)); - }*/ - public static long toStartOfMonth(Instant instant) { return instant .atZone(ZONE_ID) diff --git a/desktop/src/main/java/bisq/desktop/main/dao/economy/supply/SupplyView.java b/desktop/src/main/java/bisq/desktop/main/dao/economy/supply/SupplyView.java index 9878e6822ca..634b6af8b62 100644 --- a/desktop/src/main/java/bisq/desktop/main/dao/economy/supply/SupplyView.java +++ b/desktop/src/main/java/bisq/desktop/main/dao/economy/supply/SupplyView.java @@ -58,8 +58,7 @@ public class SupplyView extends ActivatableView implements DaoSt private final BsqFormatter bsqFormatter; private TextField genesisIssueAmountTextField, compRequestIssueAmountTextField, reimbursementAmountTextField, - totalBurntBsqTradeFeeTextField, /*totalBurntBtcTradeFeeTextField, */ - totalLockedUpAmountTextField, totalUnlockingAmountTextField, + totalBurntBsqTradeFeeTextField, totalLockedUpAmountTextField, totalUnlockingAmountTextField, totalUnlockedAmountTextField, totalConfiscatedAmountTextField, totalProofOfBurnAmountTextField; private int gridRow = 0; private long fromDate, toDate; @@ -175,14 +174,6 @@ private void createIssuedAndBurnedFields() { totalProofOfBurnAmountTextField = addTopLabelReadOnlyTextField(root, gridRow, 1, Res.get("dao.factsAndFigures.supply.proofOfBurn"), Layout.COMPACT_FIRST_ROW_AND_COMPACT_GROUP_DISTANCE).second; - - /* totalBurntBtcTradeFeeTextField = addTopLabelReadOnlyTextField(root, gridRow, 1, - Res.get("dao.factsAndFigures.supply.btcTradeFee"), Layout.COMPACT_FIRST_ROW_AND_COMPACT_GROUP_DISTANCE).second; - - Tuple3 tuple3 = addTopLabelReadOnlyTextField(root, ++gridRow, - Res.get("dao.factsAndFigures.supply.proofOfBurn")); - totalProofOfBurnAmountTextField = tuple3.second; - GridPane.setColumnSpan(tuple3.third, 2);*/ } private void createLockedBsqFields() { @@ -224,12 +215,8 @@ private void updateEconomicsData() { Coin totalBurntTradeFee = Coin.valueOf(daoEconomyDataProvider.getBsqTradeFeeAmount(fromDate, getToDate())); totalBurntBsqTradeFeeTextField.setText(bsqFormatter.formatAmountWithGroupSeparatorAndCode(totalBurntTradeFee)); - /* Coin totalBurntBtcTradeFee = Coin.valueOf(daoEconomyDataProvider.getBtcTradeFeeAmount(fromDate, getToDate())); - totalBurntBtcTradeFeeTextField.setText(bsqFormatter.formatAmountWithGroupSeparatorAndCode(totalBurntBtcTradeFee)); -*/ Coin totalProofOfBurnAmount = Coin.valueOf(daoEconomyDataProvider.getProofOfBurnAmount(fromDate, getToDate())); totalProofOfBurnAmountTextField.setText(bsqFormatter.formatAmountWithGroupSeparatorAndCode(totalProofOfBurnAmount)); - } private void updateLockedTxData() { diff --git a/desktop/src/main/java/bisq/desktop/main/dao/economy/supply/chart/DaoEconomyChartModel.java b/desktop/src/main/java/bisq/desktop/main/dao/economy/supply/chart/DaoEconomyChartModel.java index b6f7e160fec..5716029d1da 100644 --- a/desktop/src/main/java/bisq/desktop/main/dao/economy/supply/chart/DaoEconomyChartModel.java +++ b/desktop/src/main/java/bisq/desktop/main/dao/economy/supply/chart/DaoEconomyChartModel.java @@ -56,11 +56,6 @@ List> getBsqTradeFeeChartData(Predicate predi return toChartData(daoEconomyDataProvider.getBurnedBsqByMonth(daoStateService.getTradeFeeTxs(), predicate)); } - // The resulting data are not very useful. It causes negative values if burn rate > issuance in selected timeframe - /* List> getBtcTradeFeeChartData(Predicate predicate) { - return toChartData(daoEconomyDataProvider.getBurnedBtcByMonth(predicate)); - }*/ - List> getCompensationChartData(Predicate predicate) { return toChartData(daoEconomyDataProvider.getMergedCompensationMap(predicate)); } @@ -73,6 +68,20 @@ List> getReimbursementChartData(Predicate pre return toChartData(daoEconomyDataProvider.getMergedReimbursementMap(predicate)); } + List> getTotalIssuedChartData(Predicate predicate) { + Map compensationMap = daoEconomyDataProvider.getMergedCompensationMap(predicate); + Map reimbursementMap = daoEconomyDataProvider.getMergedReimbursementMap(predicate); + Map sum = daoEconomyDataProvider.getMergedMap(compensationMap, reimbursementMap, Long::sum); + return toChartData(sum); + } + + List> getTotalBurnedChartData(Predicate predicate) { + Map tradeFee = daoEconomyDataProvider.getBurnedBsqByMonth(daoStateService.getTradeFeeTxs(), predicate); + Map proofOfBurn = daoEconomyDataProvider.getBurnedBsqByMonth(daoStateService.getProofOfBurnTxs(), predicate); + Map sum = daoEconomyDataProvider.getMergedMap(tradeFee, proofOfBurn, Long::sum); + return toChartData(sum); + } + void initBounds(List> tradeFeeChartData, List> compensationRequestsChartData) { Tuple2 xMinMaxTradeFee = getMinMax(tradeFeeChartData); diff --git a/desktop/src/main/java/bisq/desktop/main/dao/economy/supply/chart/DaoEconomyChartView.java b/desktop/src/main/java/bisq/desktop/main/dao/economy/supply/chart/DaoEconomyChartView.java index 2ae739b4c7e..54346f918a1 100644 --- a/desktop/src/main/java/bisq/desktop/main/dao/economy/supply/chart/DaoEconomyChartView.java +++ b/desktop/src/main/java/bisq/desktop/main/dao/economy/supply/chart/DaoEconomyChartView.java @@ -45,6 +45,7 @@ import java.text.DecimalFormat; +import java.util.Collection; import java.util.Date; import java.util.List; import java.util.function.Predicate; @@ -57,7 +58,7 @@ public class DaoEconomyChartView extends ChartView { private final BsqFormatter bsqFormatter; private XYChart.Series seriesBsqTradeFee, seriesProofOfBurn, seriesCompensation, - seriesReimbursement/*, seriesBtcTradeFee*/; + seriesReimbursement, seriesTotalIssued, seriesTotalBurned; private ListChangeListener nodeListChangeListener; @Inject @@ -76,10 +77,11 @@ public DaoEconomyChartView(DaoEconomyChartModel model, BsqFormatter bsqFormatter public void initialize() { super.initialize(); - // Turn off some series + // Turn off detail series + hideSeries(seriesBsqTradeFee); + hideSeries(seriesCompensation); hideSeries(seriesProofOfBurn); hideSeries(seriesReimbursement); - /* hideSeries(seriesBtcTradeFee);*/ nodeListChangeListener = c -> { while (c.next()) { @@ -171,84 +173,88 @@ public Number fromString(String string) { @Override protected void addSeries() { - seriesBsqTradeFee = new XYChart.Series<>(); - seriesBsqTradeFee.setName(Res.get("dao.factsAndFigures.supply.bsqTradeFee")); - seriesIndexMap.put(seriesBsqTradeFee.getName(), 0); - chart.getData().add(seriesBsqTradeFee); + seriesTotalIssued = new XYChart.Series<>(); + seriesTotalIssued.setName(Res.get("dao.factsAndFigures.supply.totalIssued")); + seriesIndexMap.put(seriesTotalIssued.getName(), 0); + chart.getData().add(seriesTotalIssued); - /* seriesBtcTradeFee = new XYChart.Series<>(); - seriesBtcTradeFee.setName(Res.get("dao.factsAndFigures.supply.btcTradeFee")); - seriesIndexMap.put(seriesBtcTradeFee.getName(), 4); - chart.getData().add(seriesBtcTradeFee);*/ + seriesTotalBurned = new XYChart.Series<>(); + seriesTotalBurned.setName(Res.get("dao.factsAndFigures.supply.totalBurned")); + seriesIndexMap.put(seriesTotalBurned.getName(), 1); + chart.getData().add(seriesTotalBurned); seriesCompensation = new XYChart.Series<>(); seriesCompensation.setName(Res.get("dao.factsAndFigures.supply.compReq")); - seriesIndexMap.put(seriesCompensation.getName(), 1); + seriesIndexMap.put(seriesCompensation.getName(), 2); chart.getData().add(seriesCompensation); - seriesProofOfBurn = new XYChart.Series<>(); - seriesProofOfBurn.setName(Res.get("dao.factsAndFigures.supply.proofOfBurn")); - seriesIndexMap.put(seriesProofOfBurn.getName(), 2); - chart.getData().add(seriesProofOfBurn); - seriesReimbursement = new XYChart.Series<>(); seriesReimbursement.setName(Res.get("dao.factsAndFigures.supply.reimbursement")); seriesIndexMap.put(seriesReimbursement.getName(), 3); chart.getData().add(seriesReimbursement); + + seriesBsqTradeFee = new XYChart.Series<>(); + seriesBsqTradeFee.setName(Res.get("dao.factsAndFigures.supply.bsqTradeFee")); + seriesIndexMap.put(seriesBsqTradeFee.getName(), 4); + chart.getData().add(seriesBsqTradeFee); + + seriesProofOfBurn = new XYChart.Series<>(); + seriesProofOfBurn.setName(Res.get("dao.factsAndFigures.supply.proofOfBurn")); + seriesIndexMap.put(seriesProofOfBurn.getName(), 5); + chart.getData().add(seriesProofOfBurn); + } + + @Override + protected Collection> getSeriesForLegend1() { + return List.of(seriesTotalIssued, seriesCompensation, seriesReimbursement); + } + + @Override + protected Collection> getSeriesForLegend2() { + return List.of(seriesTotalBurned, seriesBsqTradeFee, seriesProofOfBurn); } @Override protected void initData() { - List> bsqTradeFeeChartData = model.getBsqTradeFeeChartData(e -> true); + Predicate predicate = e -> true; + List> bsqTradeFeeChartData = model.getBsqTradeFeeChartData(predicate); seriesBsqTradeFee.getData().setAll(bsqTradeFeeChartData); - /* List> btcTradeFeeChartData = model.getBtcTradeFeeChartData(e -> true); - seriesBtcTradeFee.getData().setAll(btcTradeFeeChartData);*/ - - List> compensationRequestsChartData = model.getCompensationChartData(e -> true); + List> compensationRequestsChartData = model.getCompensationChartData(predicate); seriesCompensation.getData().setAll(compensationRequestsChartData); - List> proofOfBurnChartData = model.getProofOfBurnChartData(e -> true); - seriesProofOfBurn.getData().setAll(proofOfBurnChartData); - - List> reimbursementChartData = model.getReimbursementChartData(e -> true); - seriesReimbursement.getData().setAll(reimbursementChartData); - - applyTooltip(); - // We don't need redundant data like reimbursementChartData as time value from compensationRequestsChartData // will cover it model.initBounds(bsqTradeFeeChartData, compensationRequestsChartData); xAxis.setLowerBound(model.getLowerBound().doubleValue()); xAxis.setUpperBound(model.getUpperBound().doubleValue()); + updateOtherSeries(predicate); + applyTooltip(); + UserThread.execute(this::setTimeLineLabels); } @Override protected void updateData(Predicate predicate) { - List> tradeFeeChartData = model.getBsqTradeFeeChartData(predicate); - seriesBsqTradeFee.getData().setAll(tradeFeeChartData); - - /* List> btcTradeFeeChartData = model.getBtcTradeFeeChartData(predicate); - seriesBtcTradeFee.getData().setAll(btcTradeFeeChartData);*/ - - List> compensationRequestsChartData = model.getCompensationChartData(predicate); - seriesCompensation.getData().setAll(compensationRequestsChartData); - - List> proofOfBurnChartData = model.getProofOfBurnChartData(predicate); - seriesProofOfBurn.getData().setAll(proofOfBurnChartData); - - List> reimbursementChartData = model.getReimbursementChartData(predicate); - seriesReimbursement.getData().setAll(reimbursementChartData); + seriesBsqTradeFee.getData().setAll(model.getBsqTradeFeeChartData(predicate)); + seriesCompensation.getData().setAll(model.getCompensationChartData(predicate)); + updateOtherSeries(predicate); applyTooltip(); } + private void updateOtherSeries(Predicate predicate) { + seriesProofOfBurn.getData().setAll(model.getProofOfBurnChartData(predicate)); + seriesReimbursement.getData().setAll(model.getReimbursementChartData(predicate)); + seriesTotalIssued.getData().setAll(model.getTotalIssuedChartData(predicate)); + seriesTotalBurned.getData().setAll(model.getTotalBurnedChartData(predicate)); + } + @Override protected void applyTooltip() { chart.getData().forEach(series -> { - String format = series == seriesCompensation || series == seriesReimbursement ? + String format = series == seriesCompensation || series == seriesReimbursement || series == seriesTotalIssued ? "dd MMM yyyy" : "MMM yyyy"; series.getData().forEach(data -> { diff --git a/desktop/src/main/java/bisq/desktop/theme-dark.css b/desktop/src/main/java/bisq/desktop/theme-dark.css index 39d296b62c8..73a55fffb19 100644 --- a/desktop/src/main/java/bisq/desktop/theme-dark.css +++ b/desktop/src/main/java/bisq/desktop/theme-dark.css @@ -135,13 +135,16 @@ -bs-white: white; -bs-prompt-text: -bs-color-gray-3; -bs-decimals: #db6300; + -bs-soft-red: #680000; + -bs-turquoise-light: #BEDB39; /* dao chart colors */ -bs-chart-dao-line1: -bs-color-green-5; -bs-chart-dao-line2: -bs-color-blue-2; -bs-chart-dao-line3: -bs-turquoise; -bs-chart-dao-line4: -bs-yellow; - -bs-chart-dao-line5: -bs-color-blue-0; + -bs-chart-dao-line5: -bs-soft-red; + -bs-chart-dao-line6: -bs-turquoise-light; /* Monero orange color code */ -xmr-orange: #f26822; diff --git a/desktop/src/main/java/bisq/desktop/theme-light.css b/desktop/src/main/java/bisq/desktop/theme-light.css index c296614e295..d3e5d21aa88 100644 --- a/desktop/src/main/java/bisq/desktop/theme-light.css +++ b/desktop/src/main/java/bisq/desktop/theme-light.css @@ -102,13 +102,16 @@ -bs-progress-bar-track: #e0e0e0; -bs-white: white; -bs-prompt-text: -fx-control-inner-background; + -bs-soft-red: #E74C3B; + -bs-turquoise-light: #00DCFF; /* dao chart colors */ -bs-chart-dao-line1: -bs-color-green-3; -bs-chart-dao-line2: -bs-color-blue-5; -bs-chart-dao-line3: -bs-turquoise; -bs-chart-dao-line4: -bs-yellow; - -bs-chart-dao-line5: -bs-color-blue-0; + -bs-chart-dao-line5: -bs-soft-red; + -bs-chart-dao-line6: -bs-turquoise-light; /* Monero orange color code */ -xmr-orange: #f26822; From 67d76cda27f6d3687b0f655a1244342ff63c6b04 Mon Sep 17 00:00:00 2001 From: chimp1984 Date: Fri, 5 Feb 2021 19:08:40 -0500 Subject: [PATCH 07/21] Add time interval selector --- .../desktop/components/chart/ChartModel.java | 20 ++- .../desktop/components/chart/ChartView.java | 163 ++++++++++++++---- .../chart/TemporalAdjusterUtil.java | 53 ++++++ .../supply/DaoEconomyDataProvider.java | 55 +++--- .../main/dao/economy/supply/SupplyView.java | 5 +- .../supply/chart/DaoEconomyChartModel.java | 19 +- .../supply/chart/DaoEconomyChartView.java | 31 +--- .../src/main/java/bisq/desktop/theme-dark.css | 14 +- .../main/java/bisq/desktop/theme-light.css | 14 +- 9 files changed, 275 insertions(+), 99 deletions(-) create mode 100644 desktop/src/main/java/bisq/desktop/components/chart/TemporalAdjusterUtil.java diff --git a/desktop/src/main/java/bisq/desktop/components/chart/ChartModel.java b/desktop/src/main/java/bisq/desktop/components/chart/ChartModel.java index 1c0ce459345..f74d29253d2 100644 --- a/desktop/src/main/java/bisq/desktop/components/chart/ChartModel.java +++ b/desktop/src/main/java/bisq/desktop/components/chart/ChartModel.java @@ -21,6 +21,8 @@ import bisq.common.util.Tuple2; +import java.time.temporal.TemporalAdjuster; + import java.util.HashSet; import java.util.Set; import java.util.function.Predicate; @@ -28,7 +30,7 @@ import lombok.extern.slf4j.Slf4j; @Slf4j -public class ChartModel extends ActivatableViewModel { +public abstract class ChartModel extends ActivatableViewModel { public interface Listener { /** * @param fromDate Epoch date in millis for earliest data @@ -40,9 +42,12 @@ public interface Listener { protected Number lowerBound, upperBound; protected final Set listeners = new HashSet<>(); + private Predicate predicate = e -> true; + public ChartModel() { } + /////////////////////////////////////////////////////////////////////////////////////////// // Lifecycle /////////////////////////////////////////////////////////////////////////////////////////// @@ -84,8 +89,17 @@ Tuple2 timelinePositionToEpochSeconds(double leftPos, double rig return new Tuple2<>(fromDateSec, toDateSec); } - Predicate getPredicate(Tuple2 fromToTuple) { - return value -> value >= fromToTuple.first && value <= fromToTuple.second; + protected abstract void applyTemporalAdjuster(TemporalAdjuster temporalAdjuster); + + protected abstract TemporalAdjuster getTemporalAdjuster(); + + Predicate getPredicate() { + return predicate; + } + + Predicate setAndGetPredicate(Tuple2 fromToTuple) { + predicate = value -> value >= fromToTuple.first && value <= fromToTuple.second; + return predicate; } void notifyListeners(Tuple2 fromToTuple) { diff --git a/desktop/src/main/java/bisq/desktop/components/chart/ChartView.java b/desktop/src/main/java/bisq/desktop/components/chart/ChartView.java index b7d2dd988d4..b886c058732 100644 --- a/desktop/src/main/java/bisq/desktop/components/chart/ChartView.java +++ b/desktop/src/main/java/bisq/desktop/components/chart/ChartView.java @@ -19,6 +19,9 @@ import bisq.desktop.common.view.ActivatableView; import bisq.desktop.components.AutoTooltipSlideToggleButton; +import bisq.desktop.components.AutoTooltipToggleButton; + +import bisq.core.locale.Res; import bisq.common.util.Tuple2; @@ -29,8 +32,12 @@ import javafx.scene.chart.XYChart; import javafx.scene.control.Label; import javafx.scene.control.SplitPane; +import javafx.scene.control.Toggle; +import javafx.scene.control.ToggleButton; +import javafx.scene.control.ToggleGroup; import javafx.scene.control.Tooltip; import javafx.scene.input.MouseEvent; +import javafx.scene.layout.AnchorPane; import javafx.scene.layout.HBox; import javafx.scene.layout.Pane; import javafx.scene.layout.Priority; @@ -44,6 +51,8 @@ import javafx.util.StringConverter; +import java.time.temporal.TemporalAdjuster; + import java.text.DateFormat; import java.text.SimpleDateFormat; @@ -54,12 +63,15 @@ import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.Set; import java.util.function.Predicate; import java.util.stream.Stream; import lombok.extern.slf4j.Slf4j; +import static bisq.desktop.util.FormBuilder.getTopLabelWithVBox; + @Slf4j public abstract class ChartView extends ActivatableView { private final Pane center; @@ -72,28 +84,27 @@ public abstract class ChartView extends ActivatableView dividerNodes = new ArrayList<>(); private final Double[] dividerPositions = new Double[]{0d, 1d}; + private final ToggleGroup timeUnitToggleGroup = new ToggleGroup(); private boolean pressed; private double x; private ChangeListener widthListener; private int maxSeriesSize; + private ChangeListener timeUnitChangeListener; + protected String dateFormatPatters = "dd MMM\nyyyy"; public ChartView(T model) { super(model); root = new VBox(); - Pane left = new Pane(); - center = new Pane(); - Pane right = new Pane(); - splitPane = new SplitPane(); - splitPane.getItems().addAll(left, center, right); + // time units + Pane timeIntervalBox = getTimeIntervalBox(); + + // chart xAxis = getXAxis(); yAxis = getYAxis(); - chart = getChart(); - addSeries(); - HBox legendBox1 = getLegendBox(getSeriesForLegend1()); Collection> seriesForLegend2 = getSeriesForLegend2(); HBox legendBox2 = null; @@ -101,8 +112,15 @@ public ChartView(T model) { legendBox2 = getLegendBox(seriesForLegend2); } + // Time navigation + Pane left = new Pane(); + center = new Pane(); + Pane right = new Pane(); + splitPane = new SplitPane(); + splitPane.getItems().addAll(left, center, right); timelineLabels = new HBox(); + // Container VBox box = new VBox(); int paddingRight = 89; int paddingLeft = 15; @@ -114,14 +132,9 @@ public ChartView(T model) { VBox.setMargin(legendBox2, new Insets(-20, paddingRight, 0, paddingLeft)); box.getChildren().add(legendBox2); } - root.getChildren().addAll(chart, box); + root.getChildren().addAll(timeIntervalBox, chart, box); } - protected abstract Collection> getSeriesForLegend1(); - - protected abstract Collection> getSeriesForLegend2(); - - /////////////////////////////////////////////////////////////////////////////////////////// // Lifecycle /////////////////////////////////////////////////////////////////////////////////////////// @@ -138,24 +151,37 @@ public void initialize() { splitPane.setDividerPosition(0, dividerPositions[0]); splitPane.setDividerPosition(1, dividerPositions[1]); }; + + timeUnitChangeListener = (observable, oldValue, newValue) -> { + TemporalAdjusterUtil.Interval interval = (TemporalAdjusterUtil.Interval) newValue.getUserData(); + applyTemporalAdjuster(interval.getAdjuster()); + }; } @Override public void activate() { root.widthProperty().addListener(widthListener); + timeUnitToggleGroup.selectedToggleProperty().addListener(timeUnitChangeListener); + splitPane.setOnMousePressed(this::onMousePressedSplitPane); splitPane.setOnMouseDragged(this::onMouseDragged); center.setOnMousePressed(this::onMousePressedCenter); center.setOnMouseReleased(this::onMouseReleasedCenter); initData(); - initDividerMouseHandlers(); + // Need to get called again here as otherwise styles are not applied correctly + applySeriesStyles(); + + TemporalAdjuster temporalAdjuster = model.getTemporalAdjuster(); + applyTemporalAdjuster(temporalAdjuster); + findToggleByTemporalAdjuster(temporalAdjuster).ifPresent(timeUnitToggleGroup::selectToggle); } @Override public void deactivate() { root.widthProperty().removeListener(widthListener); + timeUnitToggleGroup.selectedToggleProperty().removeListener(timeUnitChangeListener); splitPane.setOnMousePressed(null); splitPane.setOnMouseDragged(null); center.setOnMousePressed(null); @@ -176,6 +202,40 @@ public void removeListener(ChartModel.Listener listener) { // Customisations /////////////////////////////////////////////////////////////////////////////////////////// + protected Pane getTimeIntervalBox() { + ToggleButton year = getToggleButton(Res.get("time.year"), TemporalAdjusterUtil.Interval.YEAR, + timeUnitToggleGroup, "toggle-left"); + ToggleButton month = getToggleButton(Res.get("time.month"), TemporalAdjusterUtil.Interval.MONTH, + timeUnitToggleGroup, "toggle-center"); + ToggleButton week = getToggleButton(Res.get("time.week"), TemporalAdjusterUtil.Interval.WEEK, + timeUnitToggleGroup, "toggle-center"); + ToggleButton day = getToggleButton(Res.get("time.day"), TemporalAdjusterUtil.Interval.DAY, + timeUnitToggleGroup, "toggle-center"); + + HBox toggleBox = new HBox(); + toggleBox.setSpacing(0); + toggleBox.setAlignment(Pos.CENTER_LEFT); + toggleBox.getChildren().addAll(year, month, week, day); + + Tuple2 topLabelWithVBox = getTopLabelWithVBox(Res.get("shared.interval"), toggleBox); + AnchorPane pane = new AnchorPane(); + VBox vBox = topLabelWithVBox.second; + pane.getChildren().add(vBox); + AnchorPane.setRightAnchor(vBox, 90d); + return pane; + } + + protected ToggleButton getToggleButton(String label, + TemporalAdjusterUtil.Interval interval, + ToggleGroup toggleGroup, + String style) { + ToggleButton toggleButton = new AutoTooltipToggleButton(label); + toggleButton.setUserData(interval); + toggleButton.setToggleGroup(toggleGroup); + toggleButton.setId(style); + return toggleButton; + } + protected NumberAxis getXAxis() { NumberAxis xAxis; xAxis = new NumberAxis(); @@ -214,7 +274,7 @@ protected HBox getLegendBox(Collection> data) { toggle.setText(seriesName); toggle.setId("charts-legend-toggle" + seriesIndexMap.get(seriesName)); toggle.setSelected(true); - toggle.setOnAction(e -> onSelectToggle(series, toggle.isSelected())); + toggle.setOnAction(e -> onSelectLegendToggle(series, toggle.isSelected())); hBox.getChildren().add(toggle); }); Region spacer = new Region(); @@ -223,27 +283,32 @@ protected HBox getLegendBox(Collection> data) { return hBox; } - private void onSelectToggle(XYChart.Series series, boolean isSelected) { + protected abstract Collection> getSeriesForLegend1(); + + protected abstract Collection> getSeriesForLegend2(); + + private void onSelectLegendToggle(XYChart.Series series, boolean isSelected) { if (isSelected) { chart.getData().add(series); } else { chart.getData().remove(series); } applySeriesStyles(); + applyTooltip(); } protected void hideSeries(XYChart.Series series) { toggleBySeriesName.get(series.getName()).setSelected(false); - onSelectToggle(series, false); + onSelectLegendToggle(series, false); } protected StringConverter getTimeAxisStringConverter() { return new StringConverter<>() { @Override public String toString(Number value) { - DateFormat f = new SimpleDateFormat("YYYY-MM"); + DateFormat format = new SimpleDateFormat(dateFormatPatters); Date date = new Date(Math.round(value.doubleValue()) * 1000); - return f.format(date); + return format.format(date); } @Override @@ -267,6 +332,30 @@ public Number fromString(String string) { }; } + protected void applyTemporalAdjuster(TemporalAdjuster temporalAdjuster) { + model.applyTemporalAdjuster(temporalAdjuster); + findToggleByTemporalAdjuster(temporalAdjuster) + .map(e -> (TemporalAdjusterUtil.Interval) e.getUserData()) + .ifPresent(interval -> setDateFormatPatters(interval)); + + updateData(model.getPredicate()); + } + + private void setDateFormatPatters(TemporalAdjusterUtil.Interval interval) { + switch (interval) { + case YEAR: + dateFormatPatters = "yyyy"; + break; + case MONTH: + dateFormatPatters = "MMM\nyyyy"; + break; + default: + dateFormatPatters = "MMM dd\nyyyy"; + break; + } + + } + protected abstract void initData(); protected abstract void updateData(Predicate predicate); @@ -274,20 +363,25 @@ public Number fromString(String string) { protected void applyTooltip() { chart.getData().forEach(series -> { series.getData().forEach(data -> { - String xValue = getXAxis().getTickLabelFormatter().toString(data.getXValue()); - String yValue = getYAxis().getTickLabelFormatter().toString(data.getYValue()); Node node = data.getNode(); - Tooltip.install(node, new Tooltip(yValue + "\n" + xValue)); - - //Adding class on hover - node.setOnMouseEntered(event -> node.getStyleClass().add("onHover")); - - //Removing class on exit - node.setOnMouseExited(event -> node.getStyleClass().remove("onHover")); + if (node == null) { + return; + } + String xValue = getTooltipDateConverter(data.getXValue()); + String yValue = getYAxisStringConverter().toString(data.getYValue()); + Tooltip.install(node, new Tooltip(Res.get("dao.factsAndFigures.supply.chart.tradeFee.toolTip", yValue, xValue))); }); }); } + protected String getTooltipDateConverter(Number date) { + return getTimeAxisStringConverter().toString(date).replace("\n", " "); + } + + protected String getTooltipValueConverter(Number value) { + return getYAxisStringConverter().toString(value); + } + // Only called once when initial data are applied. We want the min. and max. values so we have the max. scale for // navigation. protected void setTimeLineLabels() { @@ -317,7 +411,7 @@ protected void setTimeLineLabels() { // The chart framework assigns the colored depending on the order it got added, but want to keep colors // the same so they match with the legend toggle. - private void applySeriesStyles() { + protected void applySeriesStyles() { for (int index = 0; index < chart.getData().size(); index++) { XYChart.Series series = chart.getData().get(index); int staticIndex = seriesIndexMap.get(series.getName()); @@ -350,6 +444,12 @@ private int getMaxSeriesSize() { return maxSeriesSize; } + private Optional findToggleByTemporalAdjuster(TemporalAdjuster adjuster) { + return timeUnitToggleGroup.getToggles().stream() + .filter(toggle -> ((TemporalAdjusterUtil.Interval) toggle.getUserData()).getAdjuster().equals(adjuster)) + .findAny(); + } + /////////////////////////////////////////////////////////////////////////////////////////// // Timeline navigation @@ -371,8 +471,7 @@ private void onTimelineChanged() { dividerPositions[1] = rightPos; splitPane.setDividerPositions(leftPos, rightPos); Tuple2 fromToTuple = model.timelinePositionToEpochSeconds(leftPos, rightPos); - updateData(model.getPredicate(fromToTuple)); - applySeriesStyles(); + updateData(model.setAndGetPredicate(fromToTuple)); model.notifyListeners(fromToTuple); } diff --git a/desktop/src/main/java/bisq/desktop/components/chart/TemporalAdjusterUtil.java b/desktop/src/main/java/bisq/desktop/components/chart/TemporalAdjusterUtil.java new file mode 100644 index 00000000000..60de3e6bf22 --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/components/chart/TemporalAdjusterUtil.java @@ -0,0 +1,53 @@ +/* + * This file is part of Bisq. + * + * Bisq is free software: you can redistribute it and/or modify it + * under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at + * your option) any later version. + * + * Bisq is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public + * License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Bisq. If not, see . + */ + +package bisq.desktop.components.chart; + +import java.time.Instant; +import java.time.ZoneId; +import java.time.temporal.TemporalAdjuster; +import java.time.temporal.TemporalAdjusters; + +import lombok.Getter; + +public class TemporalAdjusterUtil { + public enum Interval { + YEAR(TemporalAdjusters.firstDayOfYear()), + MONTH(TemporalAdjusters.firstDayOfMonth()), + WEEK(TemporalAdjusters.ofDateAdjuster(date -> date.plusWeeks(1))), + DAY(TemporalAdjusters.ofDateAdjuster(d -> d)); + + @Getter + private final TemporalAdjuster adjuster; + + Interval(TemporalAdjuster adjuster) { + this.adjuster = adjuster; + } + } + + private static final ZoneId ZONE_ID = ZoneId.systemDefault(); + + public static long toTimeInterval(Instant instant, TemporalAdjuster temporalAdjuster) { + return instant + .atZone(ZONE_ID) + .toLocalDate() + .with(temporalAdjuster) + .atStartOfDay(ZONE_ID) + .toInstant() + .getEpochSecond(); + } +} diff --git a/desktop/src/main/java/bisq/desktop/main/dao/economy/supply/DaoEconomyDataProvider.java b/desktop/src/main/java/bisq/desktop/main/dao/economy/supply/DaoEconomyDataProvider.java index 92ed0a1855b..2201efbf318 100644 --- a/desktop/src/main/java/bisq/desktop/main/dao/economy/supply/DaoEconomyDataProvider.java +++ b/desktop/src/main/java/bisq/desktop/main/dao/economy/supply/DaoEconomyDataProvider.java @@ -17,6 +17,8 @@ package bisq.desktop.main.dao.economy.supply; +import bisq.desktop.components.chart.TemporalAdjusterUtil; + import bisq.core.dao.state.DaoStateService; import bisq.core.dao.state.model.blockchain.Tx; import bisq.core.dao.state.model.governance.Issuance; @@ -26,9 +28,7 @@ import javax.inject.Singleton; import java.time.Instant; -import java.time.ZoneId; import java.time.temporal.TemporalAdjuster; -import java.time.temporal.TemporalAdjusters; import java.util.Collection; import java.util.HashMap; @@ -46,11 +46,10 @@ @Slf4j @Singleton public class DaoEconomyDataProvider { - private static final ZoneId ZONE_ID = ZoneId.systemDefault(); - private static final TemporalAdjuster FIRST_DAY_OF_MONTH = TemporalAdjusters.firstDayOfMonth(); - private final DaoStateService daoStateService; - private final Function blockHeightToEpochSeconds; + private final Function blockTimeOfIssuanceFunction; + + private TemporalAdjuster temporalAdjuster = TemporalAdjusterUtil.Interval.MONTH.getAdjuster(); /////////////////////////////////////////////////////////////////////////////////////////// @@ -62,8 +61,23 @@ public DaoEconomyDataProvider(DaoStateService daoStateService) { super(); this.daoStateService = daoStateService; - blockHeightToEpochSeconds = memoize(height -> - toStartOfMonth(Instant.ofEpochMilli(daoStateService.getBlockTime(height)))); + blockTimeOfIssuanceFunction = memoize(issuance -> { + int height = daoStateService.getStartHeightOfCurrentCycle(issuance.getChainHeight()).orElse(0); + return daoStateService.getBlockTime(height); + }); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // API + /////////////////////////////////////////////////////////////////////////////////////////// + + public void setTemporalAdjuster(TemporalAdjuster temporalAdjuster) { + this.temporalAdjuster = temporalAdjuster; + } + + public TemporalAdjuster getTemporalAdjuster() { + return temporalAdjuster; } /** @@ -109,7 +123,7 @@ public long getProofOfBurnAmount(long fromDate, long toDate) { public Map getBurnedBsqByMonth(Collection txs, Predicate predicate) { return txs.stream() - .collect(Collectors.groupingBy(tx -> toStartOfMonth(Instant.ofEpochMilli(tx.getTime())))) + .collect(Collectors.groupingBy(tx -> toTimeInterval(Instant.ofEpochMilli(tx.getTime())))) .entrySet() .stream() .filter(entry -> predicate.test(entry.getKey())) @@ -125,8 +139,8 @@ public Map getBurnedBsqByMonth(Collection txs, Predicate p // as adjuster would be more complicate (though could be done in future). public Map getIssuedBsqByMonth(Set issuanceSet, Predicate predicate) { return issuanceSet.stream() - .collect(Collectors.groupingBy(blockHeightToEpochSeconds.compose(issuance -> - daoStateService.getStartHeightOfCurrentCycle(issuance.getChainHeight()).orElse(0)))) + .collect(Collectors.groupingBy(issuance -> + toTimeInterval(Instant.ofEpochMilli(blockTimeOfIssuanceFunction.apply(issuance))))) .entrySet() .stream() .filter(entry -> predicate.test(entry.getKey())) @@ -165,18 +179,13 @@ private Map getHistoricalIssuanceByMonth(Map historicalD // as we have clearly separated that now. In case we have duplicate data for a months we use the static data. return historicalData.entrySet().stream() .filter(e -> predicate.test(e.getKey())) - .collect(Collectors.toMap(e -> toStartOfMonth(Instant.ofEpochSecond(e.getKey())), - Map.Entry::getValue)); - } - - public static long toStartOfMonth(Instant instant) { - return instant - .atZone(ZONE_ID) - .toLocalDate() - .with(FIRST_DAY_OF_MONTH) - .atStartOfDay(ZONE_ID) - .toInstant() - .getEpochSecond(); + .collect(Collectors.toMap(e -> toTimeInterval(Instant.ofEpochSecond(e.getKey())), + Map.Entry::getValue, + (a, b) -> a + b)); + } + + public long toTimeInterval(Instant instant) { + return TemporalAdjusterUtil.toTimeInterval(instant, temporalAdjuster); } /////////////////////////////////////////////////////////////////////////////////////////// diff --git a/desktop/src/main/java/bisq/desktop/main/dao/economy/supply/SupplyView.java b/desktop/src/main/java/bisq/desktop/main/dao/economy/supply/SupplyView.java index 634b6af8b62..629abb7402c 100644 --- a/desktop/src/main/java/bisq/desktop/main/dao/economy/supply/SupplyView.java +++ b/desktop/src/main/java/bisq/desktop/main/dao/economy/supply/SupplyView.java @@ -44,8 +44,6 @@ import javafx.geometry.Insets; -import java.util.Date; - import static bisq.desktop.util.FormBuilder.addTitledGroupBg; import static bisq.desktop.util.FormBuilder.addTopLabelReadOnlyTextField; @@ -104,6 +102,7 @@ protected void activate() { protected void deactivate() { daoEconomyChartView.removeListener(this); daoFacade.removeBsqStateListener(this); + daoEconomyChartView.deactivate(); } @@ -115,8 +114,6 @@ protected void deactivate() { public void onDateFilterChanged(long fromDate, long toDate) { this.fromDate = fromDate; this.toDate = toDate; - log.error(new Date(fromDate).toString()); - log.error(new Date(toDate).toString()); updateEconomicsData(); } diff --git a/desktop/src/main/java/bisq/desktop/main/dao/economy/supply/chart/DaoEconomyChartModel.java b/desktop/src/main/java/bisq/desktop/main/dao/economy/supply/chart/DaoEconomyChartModel.java index 5716029d1da..b56536a68fb 100644 --- a/desktop/src/main/java/bisq/desktop/main/dao/economy/supply/chart/DaoEconomyChartModel.java +++ b/desktop/src/main/java/bisq/desktop/main/dao/economy/supply/chart/DaoEconomyChartModel.java @@ -28,6 +28,9 @@ import javafx.scene.chart.XYChart; +import java.time.Instant; +import java.time.temporal.TemporalAdjuster; + import java.util.List; import java.util.Map; import java.util.function.Predicate; @@ -47,11 +50,21 @@ public class DaoEconomyChartModel extends ChartModel { @Inject public DaoEconomyChartModel(DaoStateService daoStateService, DaoEconomyDataProvider daoEconomyDataProvider) { super(); - this.daoStateService = daoStateService; + this.daoStateService = daoStateService; this.daoEconomyDataProvider = daoEconomyDataProvider; } + @Override + protected void applyTemporalAdjuster(TemporalAdjuster temporalAdjuster) { + daoEconomyDataProvider.setTemporalAdjuster(temporalAdjuster); + } + + @Override + protected TemporalAdjuster getTemporalAdjuster() { + return daoEconomyDataProvider.getTemporalAdjuster(); + } + List> getBsqTradeFeeChartData(Predicate predicate) { return toChartData(daoEconomyDataProvider.getBurnedBsqByMonth(daoStateService.getTradeFeeTxs(), predicate)); } @@ -91,6 +104,10 @@ void initBounds(List> tradeFeeChartData, upperBound = Math.max(xMinMaxTradeFee.second, xMinMaxCompensationRequest.second); } + long toTimeInterval(Instant ofEpochSecond) { + return daoEconomyDataProvider.toTimeInterval(ofEpochSecond); + } + /////////////////////////////////////////////////////////////////////////////////////////// // Utils /////////////////////////////////////////////////////////////////////////////////////////// diff --git a/desktop/src/main/java/bisq/desktop/main/dao/economy/supply/chart/DaoEconomyChartView.java b/desktop/src/main/java/bisq/desktop/main/dao/economy/supply/chart/DaoEconomyChartView.java index 54346f918a1..ee87127c474 100644 --- a/desktop/src/main/java/bisq/desktop/main/dao/economy/supply/chart/DaoEconomyChartView.java +++ b/desktop/src/main/java/bisq/desktop/main/dao/economy/supply/chart/DaoEconomyChartView.java @@ -18,7 +18,6 @@ package bisq.desktop.main.dao.economy.supply.chart; import bisq.desktop.components.chart.ChartView; -import bisq.desktop.main.dao.economy.supply.DaoEconomyDataProvider; import bisq.desktop.util.DisplayUtils; import bisq.core.locale.Res; @@ -32,7 +31,6 @@ import javafx.scene.chart.LineChart; import javafx.scene.chart.NumberAxis; import javafx.scene.chart.XYChart; -import javafx.scene.control.Tooltip; import javafx.scene.text.Text; import javafx.geometry.Side; @@ -145,8 +143,8 @@ protected StringConverter getTimeAxisStringConverter() { return new StringConverter<>() { @Override public String toString(Number epochSeconds) { - Date date = new Date(DaoEconomyDataProvider.toStartOfMonth(Instant.ofEpochSecond(epochSeconds.longValue())) * 1000); - return DisplayUtils.formatDateAxis(date, "dd/MMM\nyyyy"); + Date date = new Date(model.toTimeInterval(Instant.ofEpochSecond(epochSeconds.longValue())) * 1000); + return DisplayUtils.formatDateAxis(date, dateFormatPatters); } @Override @@ -171,6 +169,11 @@ public Number fromString(String string) { }; } + @Override + protected String getTooltipValueConverter(Number value) { + return bsqFormatter.formatBSQSatoshisWithCode(value.longValue()); + } + @Override protected void addSeries() { seriesTotalIssued = new XYChart.Series<>(); @@ -241,7 +244,9 @@ protected void updateData(Predicate predicate) { seriesCompensation.getData().setAll(model.getCompensationChartData(predicate)); updateOtherSeries(predicate); + applyTooltip(); + applySeriesStyles(); } private void updateOtherSeries(Predicate predicate) { @@ -250,22 +255,4 @@ private void updateOtherSeries(Predicate predicate) { seriesTotalIssued.getData().setAll(model.getTotalIssuedChartData(predicate)); seriesTotalBurned.getData().setAll(model.getTotalBurnedChartData(predicate)); } - - @Override - protected void applyTooltip() { - chart.getData().forEach(series -> { - String format = series == seriesCompensation || series == seriesReimbursement || series == seriesTotalIssued ? - "dd MMM yyyy" : - "MMM yyyy"; - series.getData().forEach(data -> { - String xValue = DisplayUtils.formatDateAxis(new Date(data.getXValue().longValue() * 1000), format); - String yValue = bsqFormatter.formatBSQSatoshisWithCode(data.getYValue().longValue()); - Node node = data.getNode(); - if (node == null) { - return; - } - Tooltip.install(node, new Tooltip(Res.get("dao.factsAndFigures.supply.chart.tradeFee.toolTip", yValue, xValue))); - }); - }); - } } diff --git a/desktop/src/main/java/bisq/desktop/theme-dark.css b/desktop/src/main/java/bisq/desktop/theme-dark.css index 73a55fffb19..42c8471ebad 100644 --- a/desktop/src/main/java/bisq/desktop/theme-dark.css +++ b/desktop/src/main/java/bisq/desktop/theme-dark.css @@ -135,16 +135,16 @@ -bs-white: white; -bs-prompt-text: -bs-color-gray-3; -bs-decimals: #db6300; - -bs-soft-red: #680000; - -bs-turquoise-light: #BEDB39; + -bs-soft-red: #aa4c3b; + -bs-turquoise-light: #00dcdd; /* dao chart colors */ - -bs-chart-dao-line1: -bs-color-green-5; - -bs-chart-dao-line2: -bs-color-blue-2; + -bs-chart-dao-line1: -bs-color-blue-5; + -bs-chart-dao-line2: -bs-color-green-3; -bs-chart-dao-line3: -bs-turquoise; - -bs-chart-dao-line4: -bs-yellow; - -bs-chart-dao-line5: -bs-soft-red; - -bs-chart-dao-line6: -bs-turquoise-light; + -bs-chart-dao-line4: -bs-turquoise-light; + -bs-chart-dao-line5: -bs-yellow; + -bs-chart-dao-line6: -bs-soft-red; /* Monero orange color code */ -xmr-orange: #f26822; diff --git a/desktop/src/main/java/bisq/desktop/theme-light.css b/desktop/src/main/java/bisq/desktop/theme-light.css index d3e5d21aa88..678e681e823 100644 --- a/desktop/src/main/java/bisq/desktop/theme-light.css +++ b/desktop/src/main/java/bisq/desktop/theme-light.css @@ -102,16 +102,16 @@ -bs-progress-bar-track: #e0e0e0; -bs-white: white; -bs-prompt-text: -fx-control-inner-background; - -bs-soft-red: #E74C3B; - -bs-turquoise-light: #00DCFF; + -bs-soft-red: #aa4c3b; + -bs-turquoise-light: #00dcdd; /* dao chart colors */ - -bs-chart-dao-line1: -bs-color-green-3; - -bs-chart-dao-line2: -bs-color-blue-5; + -bs-chart-dao-line1: -bs-color-blue-5; + -bs-chart-dao-line2: -bs-color-green-3; -bs-chart-dao-line3: -bs-turquoise; - -bs-chart-dao-line4: -bs-yellow; - -bs-chart-dao-line5: -bs-soft-red; - -bs-chart-dao-line6: -bs-turquoise-light; + -bs-chart-dao-line4: -bs-turquoise-light; + -bs-chart-dao-line5: -bs-yellow; + -bs-chart-dao-line6: -bs-soft-red; /* Monero orange color code */ -xmr-orange: #f26822; From 320a2f8a1f5ef0af0b0083f935f9b2448551a649 Mon Sep 17 00:00:00 2001 From: chimp1984 Date: Fri, 5 Feb 2021 19:41:43 -0500 Subject: [PATCH 08/21] Only calculate data for active series --- .../desktop/components/chart/ChartModel.java | 2 +- .../desktop/components/chart/ChartView.java | 28 +++++-- .../supply/chart/DaoEconomyChartView.java | 74 ++++++++++++++----- 3 files changed, 76 insertions(+), 28 deletions(-) diff --git a/desktop/src/main/java/bisq/desktop/components/chart/ChartModel.java b/desktop/src/main/java/bisq/desktop/components/chart/ChartModel.java index f74d29253d2..c97e01deafc 100644 --- a/desktop/src/main/java/bisq/desktop/components/chart/ChartModel.java +++ b/desktop/src/main/java/bisq/desktop/components/chart/ChartModel.java @@ -93,7 +93,7 @@ Tuple2 timelinePositionToEpochSeconds(double leftPos, double rig protected abstract TemporalAdjuster getTemporalAdjuster(); - Predicate getPredicate() { + public Predicate getPredicate() { return predicate; } diff --git a/desktop/src/main/java/bisq/desktop/components/chart/ChartView.java b/desktop/src/main/java/bisq/desktop/components/chart/ChartView.java index b886c058732..066fe6ea903 100644 --- a/desktop/src/main/java/bisq/desktop/components/chart/ChartView.java +++ b/desktop/src/main/java/bisq/desktop/components/chart/ChartView.java @@ -85,6 +85,7 @@ public abstract class ChartView extends ActivatableView dividerNodes = new ArrayList<>(); private final Double[] dividerPositions = new Double[]{0d, 1d}; private final ToggleGroup timeUnitToggleGroup = new ToggleGroup(); + protected final Set activeSeries = new HashSet<>(); private boolean pressed; private double x; private ChangeListener widthListener; @@ -104,7 +105,8 @@ public ChartView(T model) { xAxis = getXAxis(); yAxis = getYAxis(); chart = getChart(); - addSeries(); + initSeries(); + addActiveSeries(); HBox legendBox1 = getLegendBox(getSeriesForLegend1()); Collection> seriesForLegend2 = getSeriesForLegend2(); HBox legendBox2 = null; @@ -135,6 +137,8 @@ public ChartView(T model) { root.getChildren().addAll(timeIntervalBox, chart, box); } + protected abstract void addActiveSeries(); + /////////////////////////////////////////////////////////////////////////////////////////// // Lifecycle /////////////////////////////////////////////////////////////////////////////////////////// @@ -260,7 +264,7 @@ protected XYChart getChart() { return chart; } - protected abstract void addSeries(); + protected abstract void initSeries(); protected HBox getLegendBox(Collection> data) { HBox hBox = new HBox(); @@ -269,7 +273,7 @@ protected HBox getLegendBox(Collection> data) { AutoTooltipSlideToggleButton toggle = new AutoTooltipSlideToggleButton(); toggle.setMinWidth(200); toggle.setAlignment(Pos.TOP_LEFT); - String seriesName = series.getName(); + String seriesName = getSeriesId(series); toggleBySeriesName.put(seriesName, toggle); toggle.setText(seriesName); toggle.setId("charts-legend-toggle" + seriesIndexMap.get(seriesName)); @@ -289,16 +293,22 @@ protected HBox getLegendBox(Collection> data) { private void onSelectLegendToggle(XYChart.Series series, boolean isSelected) { if (isSelected) { - chart.getData().add(series); + activateSeries(series); } else { chart.getData().remove(series); + activeSeries.remove(getSeriesId(series)); } applySeriesStyles(); applyTooltip(); } + protected void activateSeries(XYChart.Series series) { + chart.getData().add(series); + activeSeries.add(getSeriesId(series)); + } + protected void hideSeries(XYChart.Series series) { - toggleBySeriesName.get(series.getName()).setSelected(false); + toggleBySeriesName.get(getSeriesId(series)).setSelected(false); onSelectLegendToggle(series, false); } @@ -336,7 +346,7 @@ protected void applyTemporalAdjuster(TemporalAdjuster temporalAdjuster) { model.applyTemporalAdjuster(temporalAdjuster); findToggleByTemporalAdjuster(temporalAdjuster) .map(e -> (TemporalAdjusterUtil.Interval) e.getUserData()) - .ifPresent(interval -> setDateFormatPatters(interval)); + .ifPresent(this::setDateFormatPatters); updateData(model.getPredicate()); } @@ -414,7 +424,7 @@ protected void setTimeLineLabels() { protected void applySeriesStyles() { for (int index = 0; index < chart.getData().size(); index++) { XYChart.Series series = chart.getData().get(index); - int staticIndex = seriesIndexMap.get(series.getName()); + int staticIndex = seriesIndexMap.get(getSeriesId(series)); Set lines = getNodesForStyle(series.getNode(), ".default-color%d.chart-series-line"); Stream symbols = series.getData().stream().map(XYChart.Data::getNode) .flatMap(node -> getNodesForStyle(node, ".default-color%d.chart-line-symbol").stream()); @@ -450,6 +460,10 @@ private Optional findToggleByTemporalAdjuster(TemporalAdjuster adjuster) .findAny(); } + // We use the name as id as there is no other suitable data inside series + protected String getSeriesId(XYChart.Series series) { + return series.getName(); + } /////////////////////////////////////////////////////////////////////////////////////////// // Timeline navigation diff --git a/desktop/src/main/java/bisq/desktop/main/dao/economy/supply/chart/DaoEconomyChartView.java b/desktop/src/main/java/bisq/desktop/main/dao/economy/supply/chart/DaoEconomyChartView.java index ee87127c474..b03e8e97b57 100644 --- a/desktop/src/main/java/bisq/desktop/main/dao/economy/supply/chart/DaoEconomyChartView.java +++ b/desktop/src/main/java/bisq/desktop/main/dao/economy/supply/chart/DaoEconomyChartView.java @@ -175,36 +175,41 @@ protected String getTooltipValueConverter(Number value) { } @Override - protected void addSeries() { + protected void initSeries() { seriesTotalIssued = new XYChart.Series<>(); seriesTotalIssued.setName(Res.get("dao.factsAndFigures.supply.totalIssued")); - seriesIndexMap.put(seriesTotalIssued.getName(), 0); - chart.getData().add(seriesTotalIssued); + seriesIndexMap.put(getSeriesId(seriesTotalIssued), 0); seriesTotalBurned = new XYChart.Series<>(); seriesTotalBurned.setName(Res.get("dao.factsAndFigures.supply.totalBurned")); - seriesIndexMap.put(seriesTotalBurned.getName(), 1); - chart.getData().add(seriesTotalBurned); + seriesIndexMap.put(getSeriesId(seriesTotalBurned), 1); seriesCompensation = new XYChart.Series<>(); seriesCompensation.setName(Res.get("dao.factsAndFigures.supply.compReq")); - seriesIndexMap.put(seriesCompensation.getName(), 2); - chart.getData().add(seriesCompensation); + seriesIndexMap.put(getSeriesId(seriesCompensation), 2); seriesReimbursement = new XYChart.Series<>(); seriesReimbursement.setName(Res.get("dao.factsAndFigures.supply.reimbursement")); - seriesIndexMap.put(seriesReimbursement.getName(), 3); - chart.getData().add(seriesReimbursement); + seriesIndexMap.put(getSeriesId(seriesReimbursement), 3); seriesBsqTradeFee = new XYChart.Series<>(); seriesBsqTradeFee.setName(Res.get("dao.factsAndFigures.supply.bsqTradeFee")); - seriesIndexMap.put(seriesBsqTradeFee.getName(), 4); - chart.getData().add(seriesBsqTradeFee); + seriesIndexMap.put(getSeriesId(seriesBsqTradeFee), 4); seriesProofOfBurn = new XYChart.Series<>(); seriesProofOfBurn.setName(Res.get("dao.factsAndFigures.supply.proofOfBurn")); - seriesIndexMap.put(seriesProofOfBurn.getName(), 5); - chart.getData().add(seriesProofOfBurn); + seriesIndexMap.put(getSeriesId(seriesProofOfBurn), 5); + } + + @Override + protected void addActiveSeries() { + addSeries(seriesTotalIssued); + addSeries(seriesTotalBurned); + } + + private void addSeries(XYChart.Series series) { + activeSeries.add(getSeriesId(series)); + chart.getData().add(series); } @Override @@ -233,15 +238,18 @@ protected void initData() { xAxis.setUpperBound(model.getUpperBound().doubleValue()); updateOtherSeries(predicate); - applyTooltip(); UserThread.execute(this::setTimeLineLabels); } @Override protected void updateData(Predicate predicate) { - seriesBsqTradeFee.getData().setAll(model.getBsqTradeFeeChartData(predicate)); - seriesCompensation.getData().setAll(model.getCompensationChartData(predicate)); + if (activeSeries.contains(getSeriesId(seriesBsqTradeFee))) { + seriesBsqTradeFee.getData().setAll(model.getBsqTradeFeeChartData(predicate)); + } + if (activeSeries.contains(getSeriesId(seriesCompensation))) { + seriesCompensation.getData().setAll(model.getCompensationChartData(predicate)); + } updateOtherSeries(predicate); @@ -249,10 +257,36 @@ protected void updateData(Predicate predicate) { applySeriesStyles(); } + @Override + protected void activateSeries(XYChart.Series series) { + super.activateSeries(series); + if (getSeriesId(series).equals(getSeriesId(seriesBsqTradeFee))) { + seriesBsqTradeFee.getData().setAll(model.getBsqTradeFeeChartData(model.getPredicate())); + } else if (getSeriesId(series).equals(getSeriesId(seriesCompensation))) { + seriesCompensation.getData().setAll(model.getCompensationChartData(model.getPredicate())); + } else if (getSeriesId(series).equals(getSeriesId(seriesProofOfBurn))) { + seriesProofOfBurn.getData().setAll(model.getProofOfBurnChartData(model.getPredicate())); + } else if (getSeriesId(series).equals(getSeriesId(seriesReimbursement))) { + seriesReimbursement.getData().setAll(model.getReimbursementChartData(model.getPredicate())); + } else if (getSeriesId(series).equals(getSeriesId(seriesTotalIssued))) { + seriesTotalIssued.getData().setAll(model.getTotalIssuedChartData(model.getPredicate())); + } else if (getSeriesId(series).equals(getSeriesId(seriesTotalBurned))) { + seriesTotalBurned.getData().setAll(model.getTotalBurnedChartData(model.getPredicate())); + } + } + private void updateOtherSeries(Predicate predicate) { - seriesProofOfBurn.getData().setAll(model.getProofOfBurnChartData(predicate)); - seriesReimbursement.getData().setAll(model.getReimbursementChartData(predicate)); - seriesTotalIssued.getData().setAll(model.getTotalIssuedChartData(predicate)); - seriesTotalBurned.getData().setAll(model.getTotalBurnedChartData(predicate)); + if (activeSeries.contains(getSeriesId(seriesProofOfBurn))) { + seriesProofOfBurn.getData().setAll(model.getProofOfBurnChartData(predicate)); + } + if (activeSeries.contains(getSeriesId(seriesReimbursement))) { + seriesReimbursement.getData().setAll(model.getReimbursementChartData(predicate)); + } + if (activeSeries.contains(getSeriesId(seriesTotalIssued))) { + seriesTotalIssued.getData().setAll(model.getTotalIssuedChartData(predicate)); + } + if (activeSeries.contains(getSeriesId(seriesTotalBurned))) { + seriesTotalBurned.getData().setAll(model.getTotalBurnedChartData(predicate)); + } } } From 885fff5163d1581e2261cf74596954c14de60a44 Mon Sep 17 00:00:00 2001 From: chimp1984 Date: Fri, 5 Feb 2021 21:43:34 -0500 Subject: [PATCH 09/21] Refactoring: Rename classes, package and move class --- .../main/dao/economy/supply/SupplyView.java | 35 +++++++++--------- .../DaoDataChartModel.java} | 37 +++++++++---------- .../DaoDataChartView.java} | 6 +-- .../DaoDataProvider.java} | 6 +-- 4 files changed, 42 insertions(+), 42 deletions(-) rename desktop/src/main/java/bisq/desktop/main/dao/economy/supply/{chart/DaoEconomyChartModel.java => daodata/DaoDataChartModel.java} (68%) rename desktop/src/main/java/bisq/desktop/main/dao/economy/supply/{chart/DaoEconomyChartView.java => daodata/DaoDataChartView.java} (98%) rename desktop/src/main/java/bisq/desktop/main/dao/economy/supply/{DaoEconomyDataProvider.java => daodata/DaoDataProvider.java} (98%) diff --git a/desktop/src/main/java/bisq/desktop/main/dao/economy/supply/SupplyView.java b/desktop/src/main/java/bisq/desktop/main/dao/economy/supply/SupplyView.java index 629abb7402c..7ab1169fabf 100644 --- a/desktop/src/main/java/bisq/desktop/main/dao/economy/supply/SupplyView.java +++ b/desktop/src/main/java/bisq/desktop/main/dao/economy/supply/SupplyView.java @@ -21,7 +21,8 @@ import bisq.desktop.common.view.FxmlView; import bisq.desktop.components.TitledGroupBg; import bisq.desktop.components.chart.ChartModel; -import bisq.desktop.main.dao.economy.supply.chart.DaoEconomyChartView; +import bisq.desktop.main.dao.economy.supply.daodata.DaoDataChartView; +import bisq.desktop.main.dao.economy.supply.daodata.DaoDataProvider; import bisq.desktop.util.Layout; import bisq.core.dao.DaoFacade; @@ -50,9 +51,9 @@ @FxmlView public class SupplyView extends ActivatableView implements DaoStateListener, ChartModel.Listener { private final DaoFacade daoFacade; - private final DaoEconomyChartView daoEconomyChartView; + private final DaoDataChartView daoDataChartView; // Shared model between SupplyView and RevenueChartModel - private final DaoEconomyDataProvider daoEconomyDataProvider; + private final DaoDataProvider daoDataProvider; private final BsqFormatter bsqFormatter; private TextField genesisIssueAmountTextField, compRequestIssueAmountTextField, reimbursementAmountTextField, @@ -68,12 +69,12 @@ public class SupplyView extends ActivatableView implements DaoSt @Inject private SupplyView(DaoFacade daoFacade, - DaoEconomyChartView daoEconomyChartView, - DaoEconomyDataProvider daoEconomyDataProvider, + DaoDataChartView daoDataChartView, + DaoDataProvider daoDataProvider, BsqFormatter bsqFormatter) { this.daoFacade = daoFacade; - this.daoEconomyChartView = daoEconomyChartView; - this.daoEconomyDataProvider = daoEconomyDataProvider; + this.daoDataChartView = daoDataChartView; + this.daoDataProvider = daoDataProvider; this.bsqFormatter = bsqFormatter; } @@ -93,16 +94,16 @@ protected void activate() { updateWithBsqBlockChainData(); - daoEconomyChartView.activate(); - daoEconomyChartView.addListener(this); + daoDataChartView.activate(); + daoDataChartView.addListener(this); daoFacade.addBsqStateListener(this); } @Override protected void deactivate() { - daoEconomyChartView.removeListener(this); + daoDataChartView.removeListener(this); daoFacade.removeBsqStateListener(this); - daoEconomyChartView.deactivate(); + daoDataChartView.deactivate(); } @@ -133,11 +134,11 @@ public void onParseBlockCompleteAfterBatchProcessing(Block block) { private void createChart() { addTitledGroupBg(root, gridRow, 2, Res.get("dao.factsAndFigures.supply.issuedVsBurnt")); - daoEconomyChartView.initialize(); + daoDataChartView.initialize(); AnchorPane chartPane = new AnchorPane(); chartPane.getStyleClass().add("chart-pane"); - VBox chartContainer = daoEconomyChartView.getRoot(); + VBox chartContainer = daoDataChartView.getRoot(); AnchorPane.setTopAnchor(chartContainer, 15d); AnchorPane.setBottomAnchor(chartContainer, 10d); AnchorPane.setLeftAnchor(chartContainer, 25d); @@ -203,16 +204,16 @@ private void updateWithBsqBlockChainData() { private void updateEconomicsData() { // We use the supplyDataProvider to get the adjusted data with static historical data as well to use the same // monthly scoped data. - Coin issuedAmountFromCompRequests = Coin.valueOf(daoEconomyDataProvider.getCompensationAmount(fromDate, getToDate())); + Coin issuedAmountFromCompRequests = Coin.valueOf(daoDataProvider.getCompensationAmount(fromDate, getToDate())); compRequestIssueAmountTextField.setText(bsqFormatter.formatAmountWithGroupSeparatorAndCode(issuedAmountFromCompRequests)); - Coin issuedAmountFromReimbursementRequests = Coin.valueOf(daoEconomyDataProvider.getReimbursementAmount(fromDate, getToDate())); + Coin issuedAmountFromReimbursementRequests = Coin.valueOf(daoDataProvider.getReimbursementAmount(fromDate, getToDate())); reimbursementAmountTextField.setText(bsqFormatter.formatAmountWithGroupSeparatorAndCode(issuedAmountFromReimbursementRequests)); - Coin totalBurntTradeFee = Coin.valueOf(daoEconomyDataProvider.getBsqTradeFeeAmount(fromDate, getToDate())); + Coin totalBurntTradeFee = Coin.valueOf(daoDataProvider.getBsqTradeFeeAmount(fromDate, getToDate())); totalBurntBsqTradeFeeTextField.setText(bsqFormatter.formatAmountWithGroupSeparatorAndCode(totalBurntTradeFee)); - Coin totalProofOfBurnAmount = Coin.valueOf(daoEconomyDataProvider.getProofOfBurnAmount(fromDate, getToDate())); + Coin totalProofOfBurnAmount = Coin.valueOf(daoDataProvider.getProofOfBurnAmount(fromDate, getToDate())); totalProofOfBurnAmountTextField.setText(bsqFormatter.formatAmountWithGroupSeparatorAndCode(totalProofOfBurnAmount)); } diff --git a/desktop/src/main/java/bisq/desktop/main/dao/economy/supply/chart/DaoEconomyChartModel.java b/desktop/src/main/java/bisq/desktop/main/dao/economy/supply/daodata/DaoDataChartModel.java similarity index 68% rename from desktop/src/main/java/bisq/desktop/main/dao/economy/supply/chart/DaoEconomyChartModel.java rename to desktop/src/main/java/bisq/desktop/main/dao/economy/supply/daodata/DaoDataChartModel.java index b56536a68fb..a450b577cd3 100644 --- a/desktop/src/main/java/bisq/desktop/main/dao/economy/supply/chart/DaoEconomyChartModel.java +++ b/desktop/src/main/java/bisq/desktop/main/dao/economy/supply/daodata/DaoDataChartModel.java @@ -15,10 +15,9 @@ * along with Bisq. If not, see . */ -package bisq.desktop.main.dao.economy.supply.chart; +package bisq.desktop.main.dao.economy.supply.daodata; import bisq.desktop.components.chart.ChartModel; -import bisq.desktop.main.dao.economy.supply.DaoEconomyDataProvider; import bisq.core.dao.state.DaoStateService; @@ -39,59 +38,59 @@ import lombok.extern.slf4j.Slf4j; @Slf4j -public class DaoEconomyChartModel extends ChartModel { +public class DaoDataChartModel extends ChartModel { private final DaoStateService daoStateService; - private final DaoEconomyDataProvider daoEconomyDataProvider; + private final DaoDataProvider daoDataProvider; /////////////////////////////////////////////////////////////////////////////////////////// // Constructor /////////////////////////////////////////////////////////////////////////////////////////// @Inject - public DaoEconomyChartModel(DaoStateService daoStateService, DaoEconomyDataProvider daoEconomyDataProvider) { + public DaoDataChartModel(DaoStateService daoStateService, DaoDataProvider daoDataProvider) { super(); this.daoStateService = daoStateService; - this.daoEconomyDataProvider = daoEconomyDataProvider; + this.daoDataProvider = daoDataProvider; } @Override protected void applyTemporalAdjuster(TemporalAdjuster temporalAdjuster) { - daoEconomyDataProvider.setTemporalAdjuster(temporalAdjuster); + daoDataProvider.setTemporalAdjuster(temporalAdjuster); } @Override protected TemporalAdjuster getTemporalAdjuster() { - return daoEconomyDataProvider.getTemporalAdjuster(); + return daoDataProvider.getTemporalAdjuster(); } List> getBsqTradeFeeChartData(Predicate predicate) { - return toChartData(daoEconomyDataProvider.getBurnedBsqByMonth(daoStateService.getTradeFeeTxs(), predicate)); + return toChartData(daoDataProvider.getBurnedBsqByMonth(daoStateService.getTradeFeeTxs(), predicate)); } List> getCompensationChartData(Predicate predicate) { - return toChartData(daoEconomyDataProvider.getMergedCompensationMap(predicate)); + return toChartData(daoDataProvider.getMergedCompensationMap(predicate)); } List> getProofOfBurnChartData(Predicate predicate) { - return toChartData(daoEconomyDataProvider.getBurnedBsqByMonth(daoStateService.getProofOfBurnTxs(), predicate)); + return toChartData(daoDataProvider.getBurnedBsqByMonth(daoStateService.getProofOfBurnTxs(), predicate)); } List> getReimbursementChartData(Predicate predicate) { - return toChartData(daoEconomyDataProvider.getMergedReimbursementMap(predicate)); + return toChartData(daoDataProvider.getMergedReimbursementMap(predicate)); } List> getTotalIssuedChartData(Predicate predicate) { - Map compensationMap = daoEconomyDataProvider.getMergedCompensationMap(predicate); - Map reimbursementMap = daoEconomyDataProvider.getMergedReimbursementMap(predicate); - Map sum = daoEconomyDataProvider.getMergedMap(compensationMap, reimbursementMap, Long::sum); + Map compensationMap = daoDataProvider.getMergedCompensationMap(predicate); + Map reimbursementMap = daoDataProvider.getMergedReimbursementMap(predicate); + Map sum = daoDataProvider.getMergedMap(compensationMap, reimbursementMap, Long::sum); return toChartData(sum); } List> getTotalBurnedChartData(Predicate predicate) { - Map tradeFee = daoEconomyDataProvider.getBurnedBsqByMonth(daoStateService.getTradeFeeTxs(), predicate); - Map proofOfBurn = daoEconomyDataProvider.getBurnedBsqByMonth(daoStateService.getProofOfBurnTxs(), predicate); - Map sum = daoEconomyDataProvider.getMergedMap(tradeFee, proofOfBurn, Long::sum); + Map tradeFee = daoDataProvider.getBurnedBsqByMonth(daoStateService.getTradeFeeTxs(), predicate); + Map proofOfBurn = daoDataProvider.getBurnedBsqByMonth(daoStateService.getProofOfBurnTxs(), predicate); + Map sum = daoDataProvider.getMergedMap(tradeFee, proofOfBurn, Long::sum); return toChartData(sum); } @@ -105,7 +104,7 @@ void initBounds(List> tradeFeeChartData, } long toTimeInterval(Instant ofEpochSecond) { - return daoEconomyDataProvider.toTimeInterval(ofEpochSecond); + return daoDataProvider.toTimeInterval(ofEpochSecond); } /////////////////////////////////////////////////////////////////////////////////////////// diff --git a/desktop/src/main/java/bisq/desktop/main/dao/economy/supply/chart/DaoEconomyChartView.java b/desktop/src/main/java/bisq/desktop/main/dao/economy/supply/daodata/DaoDataChartView.java similarity index 98% rename from desktop/src/main/java/bisq/desktop/main/dao/economy/supply/chart/DaoEconomyChartView.java rename to desktop/src/main/java/bisq/desktop/main/dao/economy/supply/daodata/DaoDataChartView.java index b03e8e97b57..75977cda65f 100644 --- a/desktop/src/main/java/bisq/desktop/main/dao/economy/supply/chart/DaoEconomyChartView.java +++ b/desktop/src/main/java/bisq/desktop/main/dao/economy/supply/daodata/DaoDataChartView.java @@ -15,7 +15,7 @@ * along with Bisq. If not, see . */ -package bisq.desktop.main.dao.economy.supply.chart; +package bisq.desktop.main.dao.economy.supply.daodata; import bisq.desktop.components.chart.ChartView; import bisq.desktop.util.DisplayUtils; @@ -51,7 +51,7 @@ import lombok.extern.slf4j.Slf4j; @Slf4j -public class DaoEconomyChartView extends ChartView { +public class DaoDataChartView extends ChartView { private static final DecimalFormat priceFormat = new DecimalFormat(",###"); private final BsqFormatter bsqFormatter; @@ -60,7 +60,7 @@ public class DaoEconomyChartView extends ChartView { private ListChangeListener nodeListChangeListener; @Inject - public DaoEconomyChartView(DaoEconomyChartModel model, BsqFormatter bsqFormatter) { + public DaoDataChartView(DaoDataChartModel model, BsqFormatter bsqFormatter) { super(model); this.bsqFormatter = bsqFormatter; diff --git a/desktop/src/main/java/bisq/desktop/main/dao/economy/supply/DaoEconomyDataProvider.java b/desktop/src/main/java/bisq/desktop/main/dao/economy/supply/daodata/DaoDataProvider.java similarity index 98% rename from desktop/src/main/java/bisq/desktop/main/dao/economy/supply/DaoEconomyDataProvider.java rename to desktop/src/main/java/bisq/desktop/main/dao/economy/supply/daodata/DaoDataProvider.java index 2201efbf318..642222fb4de 100644 --- a/desktop/src/main/java/bisq/desktop/main/dao/economy/supply/DaoEconomyDataProvider.java +++ b/desktop/src/main/java/bisq/desktop/main/dao/economy/supply/daodata/DaoDataProvider.java @@ -15,7 +15,7 @@ * along with Bisq. If not, see . */ -package bisq.desktop.main.dao.economy.supply; +package bisq.desktop.main.dao.economy.supply.daodata; import bisq.desktop.components.chart.TemporalAdjusterUtil; @@ -45,7 +45,7 @@ @Slf4j @Singleton -public class DaoEconomyDataProvider { +public class DaoDataProvider { private final DaoStateService daoStateService; private final Function blockTimeOfIssuanceFunction; @@ -57,7 +57,7 @@ public class DaoEconomyDataProvider { /////////////////////////////////////////////////////////////////////////////////////////// @Inject - public DaoEconomyDataProvider(DaoStateService daoStateService) { + public DaoDataProvider(DaoStateService daoStateService) { super(); this.daoStateService = daoStateService; From a332e603fb90fe74a69cc710630f66b090cdf0b5 Mon Sep 17 00:00:00 2001 From: chimp1984 Date: Fri, 5 Feb 2021 21:45:26 -0500 Subject: [PATCH 10/21] Refactoring: Rename class --- .../main/dao/economy/supply/SupplyView.java | 16 +++++----- .../supply/daodata/DaoDataChartModel.java | 32 +++++++++---------- ...DaoDataProvider.java => DaoDataModel.java} | 4 +-- 3 files changed, 26 insertions(+), 26 deletions(-) rename desktop/src/main/java/bisq/desktop/main/dao/economy/supply/daodata/{DaoDataProvider.java => DaoDataModel.java} (99%) diff --git a/desktop/src/main/java/bisq/desktop/main/dao/economy/supply/SupplyView.java b/desktop/src/main/java/bisq/desktop/main/dao/economy/supply/SupplyView.java index 7ab1169fabf..b5118af6049 100644 --- a/desktop/src/main/java/bisq/desktop/main/dao/economy/supply/SupplyView.java +++ b/desktop/src/main/java/bisq/desktop/main/dao/economy/supply/SupplyView.java @@ -22,7 +22,7 @@ import bisq.desktop.components.TitledGroupBg; import bisq.desktop.components.chart.ChartModel; import bisq.desktop.main.dao.economy.supply.daodata.DaoDataChartView; -import bisq.desktop.main.dao.economy.supply.daodata.DaoDataProvider; +import bisq.desktop.main.dao.economy.supply.daodata.DaoDataModel; import bisq.desktop.util.Layout; import bisq.core.dao.DaoFacade; @@ -53,7 +53,7 @@ public class SupplyView extends ActivatableView implements DaoSt private final DaoFacade daoFacade; private final DaoDataChartView daoDataChartView; // Shared model between SupplyView and RevenueChartModel - private final DaoDataProvider daoDataProvider; + private final DaoDataModel daoDataModel; private final BsqFormatter bsqFormatter; private TextField genesisIssueAmountTextField, compRequestIssueAmountTextField, reimbursementAmountTextField, @@ -70,11 +70,11 @@ public class SupplyView extends ActivatableView implements DaoSt @Inject private SupplyView(DaoFacade daoFacade, DaoDataChartView daoDataChartView, - DaoDataProvider daoDataProvider, + DaoDataModel daoDataModel, BsqFormatter bsqFormatter) { this.daoFacade = daoFacade; this.daoDataChartView = daoDataChartView; - this.daoDataProvider = daoDataProvider; + this.daoDataModel = daoDataModel; this.bsqFormatter = bsqFormatter; } @@ -204,16 +204,16 @@ private void updateWithBsqBlockChainData() { private void updateEconomicsData() { // We use the supplyDataProvider to get the adjusted data with static historical data as well to use the same // monthly scoped data. - Coin issuedAmountFromCompRequests = Coin.valueOf(daoDataProvider.getCompensationAmount(fromDate, getToDate())); + Coin issuedAmountFromCompRequests = Coin.valueOf(daoDataModel.getCompensationAmount(fromDate, getToDate())); compRequestIssueAmountTextField.setText(bsqFormatter.formatAmountWithGroupSeparatorAndCode(issuedAmountFromCompRequests)); - Coin issuedAmountFromReimbursementRequests = Coin.valueOf(daoDataProvider.getReimbursementAmount(fromDate, getToDate())); + Coin issuedAmountFromReimbursementRequests = Coin.valueOf(daoDataModel.getReimbursementAmount(fromDate, getToDate())); reimbursementAmountTextField.setText(bsqFormatter.formatAmountWithGroupSeparatorAndCode(issuedAmountFromReimbursementRequests)); - Coin totalBurntTradeFee = Coin.valueOf(daoDataProvider.getBsqTradeFeeAmount(fromDate, getToDate())); + Coin totalBurntTradeFee = Coin.valueOf(daoDataModel.getBsqTradeFeeAmount(fromDate, getToDate())); totalBurntBsqTradeFeeTextField.setText(bsqFormatter.formatAmountWithGroupSeparatorAndCode(totalBurntTradeFee)); - Coin totalProofOfBurnAmount = Coin.valueOf(daoDataProvider.getProofOfBurnAmount(fromDate, getToDate())); + Coin totalProofOfBurnAmount = Coin.valueOf(daoDataModel.getProofOfBurnAmount(fromDate, getToDate())); totalProofOfBurnAmountTextField.setText(bsqFormatter.formatAmountWithGroupSeparatorAndCode(totalProofOfBurnAmount)); } diff --git a/desktop/src/main/java/bisq/desktop/main/dao/economy/supply/daodata/DaoDataChartModel.java b/desktop/src/main/java/bisq/desktop/main/dao/economy/supply/daodata/DaoDataChartModel.java index a450b577cd3..2c0fb1f49bf 100644 --- a/desktop/src/main/java/bisq/desktop/main/dao/economy/supply/daodata/DaoDataChartModel.java +++ b/desktop/src/main/java/bisq/desktop/main/dao/economy/supply/daodata/DaoDataChartModel.java @@ -40,57 +40,57 @@ @Slf4j public class DaoDataChartModel extends ChartModel { private final DaoStateService daoStateService; - private final DaoDataProvider daoDataProvider; + private final DaoDataModel daoDataModel; /////////////////////////////////////////////////////////////////////////////////////////// // Constructor /////////////////////////////////////////////////////////////////////////////////////////// @Inject - public DaoDataChartModel(DaoStateService daoStateService, DaoDataProvider daoDataProvider) { + public DaoDataChartModel(DaoStateService daoStateService, DaoDataModel daoDataModel) { super(); this.daoStateService = daoStateService; - this.daoDataProvider = daoDataProvider; + this.daoDataModel = daoDataModel; } @Override protected void applyTemporalAdjuster(TemporalAdjuster temporalAdjuster) { - daoDataProvider.setTemporalAdjuster(temporalAdjuster); + daoDataModel.setTemporalAdjuster(temporalAdjuster); } @Override protected TemporalAdjuster getTemporalAdjuster() { - return daoDataProvider.getTemporalAdjuster(); + return daoDataModel.getTemporalAdjuster(); } List> getBsqTradeFeeChartData(Predicate predicate) { - return toChartData(daoDataProvider.getBurnedBsqByMonth(daoStateService.getTradeFeeTxs(), predicate)); + return toChartData(daoDataModel.getBurnedBsqByMonth(daoStateService.getTradeFeeTxs(), predicate)); } List> getCompensationChartData(Predicate predicate) { - return toChartData(daoDataProvider.getMergedCompensationMap(predicate)); + return toChartData(daoDataModel.getMergedCompensationMap(predicate)); } List> getProofOfBurnChartData(Predicate predicate) { - return toChartData(daoDataProvider.getBurnedBsqByMonth(daoStateService.getProofOfBurnTxs(), predicate)); + return toChartData(daoDataModel.getBurnedBsqByMonth(daoStateService.getProofOfBurnTxs(), predicate)); } List> getReimbursementChartData(Predicate predicate) { - return toChartData(daoDataProvider.getMergedReimbursementMap(predicate)); + return toChartData(daoDataModel.getMergedReimbursementMap(predicate)); } List> getTotalIssuedChartData(Predicate predicate) { - Map compensationMap = daoDataProvider.getMergedCompensationMap(predicate); - Map reimbursementMap = daoDataProvider.getMergedReimbursementMap(predicate); - Map sum = daoDataProvider.getMergedMap(compensationMap, reimbursementMap, Long::sum); + Map compensationMap = daoDataModel.getMergedCompensationMap(predicate); + Map reimbursementMap = daoDataModel.getMergedReimbursementMap(predicate); + Map sum = daoDataModel.getMergedMap(compensationMap, reimbursementMap, Long::sum); return toChartData(sum); } List> getTotalBurnedChartData(Predicate predicate) { - Map tradeFee = daoDataProvider.getBurnedBsqByMonth(daoStateService.getTradeFeeTxs(), predicate); - Map proofOfBurn = daoDataProvider.getBurnedBsqByMonth(daoStateService.getProofOfBurnTxs(), predicate); - Map sum = daoDataProvider.getMergedMap(tradeFee, proofOfBurn, Long::sum); + Map tradeFee = daoDataModel.getBurnedBsqByMonth(daoStateService.getTradeFeeTxs(), predicate); + Map proofOfBurn = daoDataModel.getBurnedBsqByMonth(daoStateService.getProofOfBurnTxs(), predicate); + Map sum = daoDataModel.getMergedMap(tradeFee, proofOfBurn, Long::sum); return toChartData(sum); } @@ -104,7 +104,7 @@ void initBounds(List> tradeFeeChartData, } long toTimeInterval(Instant ofEpochSecond) { - return daoDataProvider.toTimeInterval(ofEpochSecond); + return daoDataModel.toTimeInterval(ofEpochSecond); } /////////////////////////////////////////////////////////////////////////////////////////// diff --git a/desktop/src/main/java/bisq/desktop/main/dao/economy/supply/daodata/DaoDataProvider.java b/desktop/src/main/java/bisq/desktop/main/dao/economy/supply/daodata/DaoDataModel.java similarity index 99% rename from desktop/src/main/java/bisq/desktop/main/dao/economy/supply/daodata/DaoDataProvider.java rename to desktop/src/main/java/bisq/desktop/main/dao/economy/supply/daodata/DaoDataModel.java index 642222fb4de..374814d7967 100644 --- a/desktop/src/main/java/bisq/desktop/main/dao/economy/supply/daodata/DaoDataProvider.java +++ b/desktop/src/main/java/bisq/desktop/main/dao/economy/supply/daodata/DaoDataModel.java @@ -45,7 +45,7 @@ @Slf4j @Singleton -public class DaoDataProvider { +public class DaoDataModel { private final DaoStateService daoStateService; private final Function blockTimeOfIssuanceFunction; @@ -57,7 +57,7 @@ public class DaoDataProvider { /////////////////////////////////////////////////////////////////////////////////////////// @Inject - public DaoDataProvider(DaoStateService daoStateService) { + public DaoDataModel(DaoStateService daoStateService) { super(); this.daoStateService = daoStateService; From df6d451b805fd66789e7763cb089c0aef683c3a7 Mon Sep 17 00:00:00 2001 From: chimp1984 Date: Fri, 5 Feb 2021 22:06:27 -0500 Subject: [PATCH 11/21] Extract code to TemporalAdjusterModel Move TemporalAdjusterUtil code to TemporalAdjusterModel --- .../desktop/components/chart/ChartView.java | 18 ++++++------ ...erUtil.java => TemporalAdjusterModel.java} | 28 +++++++++++++++++-- .../economy/supply/daodata/DaoDataModel.java | 27 ++++++++---------- 3 files changed, 45 insertions(+), 28 deletions(-) rename desktop/src/main/java/bisq/desktop/components/chart/{TemporalAdjusterUtil.java => TemporalAdjusterModel.java} (67%) diff --git a/desktop/src/main/java/bisq/desktop/components/chart/ChartView.java b/desktop/src/main/java/bisq/desktop/components/chart/ChartView.java index 066fe6ea903..517eaba82b1 100644 --- a/desktop/src/main/java/bisq/desktop/components/chart/ChartView.java +++ b/desktop/src/main/java/bisq/desktop/components/chart/ChartView.java @@ -157,7 +157,7 @@ public void initialize() { }; timeUnitChangeListener = (observable, oldValue, newValue) -> { - TemporalAdjusterUtil.Interval interval = (TemporalAdjusterUtil.Interval) newValue.getUserData(); + TemporalAdjusterModel.Interval interval = (TemporalAdjusterModel.Interval) newValue.getUserData(); applyTemporalAdjuster(interval.getAdjuster()); }; } @@ -207,13 +207,13 @@ public void removeListener(ChartModel.Listener listener) { /////////////////////////////////////////////////////////////////////////////////////////// protected Pane getTimeIntervalBox() { - ToggleButton year = getToggleButton(Res.get("time.year"), TemporalAdjusterUtil.Interval.YEAR, + ToggleButton year = getToggleButton(Res.get("time.year"), TemporalAdjusterModel.Interval.YEAR, timeUnitToggleGroup, "toggle-left"); - ToggleButton month = getToggleButton(Res.get("time.month"), TemporalAdjusterUtil.Interval.MONTH, + ToggleButton month = getToggleButton(Res.get("time.month"), TemporalAdjusterModel.Interval.MONTH, timeUnitToggleGroup, "toggle-center"); - ToggleButton week = getToggleButton(Res.get("time.week"), TemporalAdjusterUtil.Interval.WEEK, + ToggleButton week = getToggleButton(Res.get("time.week"), TemporalAdjusterModel.Interval.WEEK, timeUnitToggleGroup, "toggle-center"); - ToggleButton day = getToggleButton(Res.get("time.day"), TemporalAdjusterUtil.Interval.DAY, + ToggleButton day = getToggleButton(Res.get("time.day"), TemporalAdjusterModel.Interval.DAY, timeUnitToggleGroup, "toggle-center"); HBox toggleBox = new HBox(); @@ -230,7 +230,7 @@ protected Pane getTimeIntervalBox() { } protected ToggleButton getToggleButton(String label, - TemporalAdjusterUtil.Interval interval, + TemporalAdjusterModel.Interval interval, ToggleGroup toggleGroup, String style) { ToggleButton toggleButton = new AutoTooltipToggleButton(label); @@ -345,13 +345,13 @@ public Number fromString(String string) { protected void applyTemporalAdjuster(TemporalAdjuster temporalAdjuster) { model.applyTemporalAdjuster(temporalAdjuster); findToggleByTemporalAdjuster(temporalAdjuster) - .map(e -> (TemporalAdjusterUtil.Interval) e.getUserData()) + .map(e -> (TemporalAdjusterModel.Interval) e.getUserData()) .ifPresent(this::setDateFormatPatters); updateData(model.getPredicate()); } - private void setDateFormatPatters(TemporalAdjusterUtil.Interval interval) { + private void setDateFormatPatters(TemporalAdjusterModel.Interval interval) { switch (interval) { case YEAR: dateFormatPatters = "yyyy"; @@ -456,7 +456,7 @@ private int getMaxSeriesSize() { private Optional findToggleByTemporalAdjuster(TemporalAdjuster adjuster) { return timeUnitToggleGroup.getToggles().stream() - .filter(toggle -> ((TemporalAdjusterUtil.Interval) toggle.getUserData()).getAdjuster().equals(adjuster)) + .filter(toggle -> ((TemporalAdjusterModel.Interval) toggle.getUserData()).getAdjuster().equals(adjuster)) .findAny(); } diff --git a/desktop/src/main/java/bisq/desktop/components/chart/TemporalAdjusterUtil.java b/desktop/src/main/java/bisq/desktop/components/chart/TemporalAdjusterModel.java similarity index 67% rename from desktop/src/main/java/bisq/desktop/components/chart/TemporalAdjusterUtil.java rename to desktop/src/main/java/bisq/desktop/components/chart/TemporalAdjusterModel.java index 60de3e6bf22..fd8634caf67 100644 --- a/desktop/src/main/java/bisq/desktop/components/chart/TemporalAdjusterUtil.java +++ b/desktop/src/main/java/bisq/desktop/components/chart/TemporalAdjusterModel.java @@ -22,9 +22,15 @@ import java.time.temporal.TemporalAdjuster; import java.time.temporal.TemporalAdjusters; +import java.util.function.Predicate; + import lombok.Getter; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +public class TemporalAdjusterModel { + private static final ZoneId ZONE_ID = ZoneId.systemDefault(); -public class TemporalAdjusterUtil { public enum Interval { YEAR(TemporalAdjusters.firstDayOfYear()), MONTH(TemporalAdjusters.firstDayOfMonth()), @@ -39,9 +45,21 @@ public enum Interval { } } - private static final ZoneId ZONE_ID = ZoneId.systemDefault(); + protected TemporalAdjuster temporalAdjuster = Interval.MONTH.getAdjuster(); + + public void setTemporalAdjuster(TemporalAdjuster temporalAdjuster) { + this.temporalAdjuster = temporalAdjuster; + } - public static long toTimeInterval(Instant instant, TemporalAdjuster temporalAdjuster) { + public TemporalAdjuster getTemporalAdjuster() { + return temporalAdjuster; + } + + public long toTimeInterval(Instant instant) { + return toTimeInterval(instant, temporalAdjuster); + } + + public long toTimeInterval(Instant instant, TemporalAdjuster temporalAdjuster) { return instant .atZone(ZONE_ID) .toLocalDate() @@ -50,4 +68,8 @@ public static long toTimeInterval(Instant instant, TemporalAdjuster temporalAdju .toInstant() .getEpochSecond(); } + + public Predicate getPredicate(long fromDate, long toDate) { + return value -> value >= fromDate / 1000 && value <= toDate / 1000; + } } diff --git a/desktop/src/main/java/bisq/desktop/main/dao/economy/supply/daodata/DaoDataModel.java b/desktop/src/main/java/bisq/desktop/main/dao/economy/supply/daodata/DaoDataModel.java index 374814d7967..51baf657fa0 100644 --- a/desktop/src/main/java/bisq/desktop/main/dao/economy/supply/daodata/DaoDataModel.java +++ b/desktop/src/main/java/bisq/desktop/main/dao/economy/supply/daodata/DaoDataModel.java @@ -17,7 +17,7 @@ package bisq.desktop.main.dao.economy.supply.daodata; -import bisq.desktop.components.chart.TemporalAdjusterUtil; +import bisq.desktop.components.chart.TemporalAdjusterModel; import bisq.core.dao.state.DaoStateService; import bisq.core.dao.state.model.blockchain.Tx; @@ -48,16 +48,14 @@ public class DaoDataModel { private final DaoStateService daoStateService; private final Function blockTimeOfIssuanceFunction; - - private TemporalAdjuster temporalAdjuster = TemporalAdjusterUtil.Interval.MONTH.getAdjuster(); - + private final TemporalAdjusterModel temporalAdjusterModel; /////////////////////////////////////////////////////////////////////////////////////////// // Constructor /////////////////////////////////////////////////////////////////////////////////////////// @Inject - public DaoDataModel(DaoStateService daoStateService) { + public DaoDataModel(DaoStateService daoStateService, TemporalAdjusterModel temporalAdjusterModel) { super(); this.daoStateService = daoStateService; @@ -65,6 +63,7 @@ public DaoDataModel(DaoStateService daoStateService) { int height = daoStateService.getStartHeightOfCurrentCycle(issuance.getChainHeight()).orElse(0); return daoStateService.getBlockTime(height); }); + this.temporalAdjusterModel = temporalAdjusterModel; } @@ -73,11 +72,11 @@ public DaoDataModel(DaoStateService daoStateService) { /////////////////////////////////////////////////////////////////////////////////////////// public void setTemporalAdjuster(TemporalAdjuster temporalAdjuster) { - this.temporalAdjuster = temporalAdjuster; + temporalAdjusterModel.setTemporalAdjuster(temporalAdjuster); } public TemporalAdjuster getTemporalAdjuster() { - return temporalAdjuster; + return temporalAdjusterModel.getTemporalAdjuster(); } /** @@ -85,7 +84,7 @@ public TemporalAdjuster getTemporalAdjuster() { * @param toDate Epoch in millis */ public long getCompensationAmount(long fromDate, long toDate) { - return getMergedCompensationMap(getPredicate(fromDate, toDate)).values().stream() + return getMergedCompensationMap(temporalAdjusterModel.getPredicate(fromDate, toDate)).values().stream() .mapToLong(e -> e) .sum(); } @@ -95,7 +94,7 @@ public long getCompensationAmount(long fromDate, long toDate) { * @param toDate Epoch in millis */ public long getReimbursementAmount(long fromDate, long toDate) { - return getMergedReimbursementMap(getPredicate(fromDate, toDate)).values().stream() + return getMergedReimbursementMap(temporalAdjusterModel.getPredicate(fromDate, toDate)).values().stream() .mapToLong(e -> e) .sum(); } @@ -105,7 +104,7 @@ public long getReimbursementAmount(long fromDate, long toDate) { * @param toDate Epoch in millis */ public long getBsqTradeFeeAmount(long fromDate, long toDate) { - return getBurnedBsqByMonth(daoStateService.getTradeFeeTxs(), getPredicate(fromDate, toDate)).values() + return getBurnedBsqByMonth(daoStateService.getTradeFeeTxs(), temporalAdjusterModel.getPredicate(fromDate, toDate)).values() .stream() .mapToLong(e -> e) .sum(); @@ -116,7 +115,7 @@ public long getBsqTradeFeeAmount(long fromDate, long toDate) { * @param toDate Epoch in millis */ public long getProofOfBurnAmount(long fromDate, long toDate) { - return getBurnedBsqByMonth(daoStateService.getProofOfBurnTxs(), getPredicate(fromDate, toDate)).values().stream() + return getBurnedBsqByMonth(daoStateService.getProofOfBurnTxs(), temporalAdjusterModel.getPredicate(fromDate, toDate)).values().stream() .mapToLong(e -> e) .sum(); } @@ -185,17 +184,13 @@ private Map getHistoricalIssuanceByMonth(Map historicalD } public long toTimeInterval(Instant instant) { - return TemporalAdjusterUtil.toTimeInterval(instant, temporalAdjuster); + return temporalAdjusterModel.toTimeInterval(instant); } /////////////////////////////////////////////////////////////////////////////////////////// // Utils /////////////////////////////////////////////////////////////////////////////////////////// - private static Predicate getPredicate(long fromDate, long toDate) { - return value -> value >= fromDate / 1000 && value <= toDate / 1000; - } - private static Function memoize(Function fn) { Map map = new ConcurrentHashMap<>(); return x -> map.computeIfAbsent(x, fn); From 4244f807c773df2dcb36d52408639d15362bfb44 Mon Sep 17 00:00:00 2001 From: chimp1984 Date: Fri, 5 Feb 2021 22:08:19 -0500 Subject: [PATCH 12/21] Refactoring: Rename classes --- .../desktop/components/chart/ChartView.java | 6 +-- .../{ChartModel.java => ChartViewModel.java} | 4 +- .../main/dao/economy/supply/SupplyView.java | 40 +++++++++---------- ...oDataModel.java => DaoChartDataModel.java} | 4 +- ...aoDataChartView.java => DaoChartView.java} | 4 +- ...ChartModel.java => DaoChartViewModel.java} | 36 ++++++++--------- 6 files changed, 47 insertions(+), 47 deletions(-) rename desktop/src/main/java/bisq/desktop/components/chart/{ChartModel.java => ChartViewModel.java} (97%) rename desktop/src/main/java/bisq/desktop/main/dao/economy/supply/daodata/{DaoDataModel.java => DaoChartDataModel.java} (98%) rename desktop/src/main/java/bisq/desktop/main/dao/economy/supply/daodata/{DaoDataChartView.java => DaoChartView.java} (98%) rename desktop/src/main/java/bisq/desktop/main/dao/economy/supply/daodata/{DaoDataChartModel.java => DaoChartViewModel.java} (71%) diff --git a/desktop/src/main/java/bisq/desktop/components/chart/ChartView.java b/desktop/src/main/java/bisq/desktop/components/chart/ChartView.java index 517eaba82b1..3812ca45399 100644 --- a/desktop/src/main/java/bisq/desktop/components/chart/ChartView.java +++ b/desktop/src/main/java/bisq/desktop/components/chart/ChartView.java @@ -73,7 +73,7 @@ import static bisq.desktop.util.FormBuilder.getTopLabelWithVBox; @Slf4j -public abstract class ChartView extends ActivatableView { +public abstract class ChartView extends ActivatableView { private final Pane center; private final SplitPane splitPane; protected final NumberAxis xAxis; @@ -194,11 +194,11 @@ public void deactivate() { dividerNodes.forEach(node -> node.setOnMouseReleased(null)); } - public void addListener(ChartModel.Listener listener) { + public void addListener(ChartViewModel.Listener listener) { model.addListener(listener); } - public void removeListener(ChartModel.Listener listener) { + public void removeListener(ChartViewModel.Listener listener) { model.removeListener(listener); } diff --git a/desktop/src/main/java/bisq/desktop/components/chart/ChartModel.java b/desktop/src/main/java/bisq/desktop/components/chart/ChartViewModel.java similarity index 97% rename from desktop/src/main/java/bisq/desktop/components/chart/ChartModel.java rename to desktop/src/main/java/bisq/desktop/components/chart/ChartViewModel.java index c97e01deafc..c5638392206 100644 --- a/desktop/src/main/java/bisq/desktop/components/chart/ChartModel.java +++ b/desktop/src/main/java/bisq/desktop/components/chart/ChartViewModel.java @@ -30,7 +30,7 @@ import lombok.extern.slf4j.Slf4j; @Slf4j -public abstract class ChartModel extends ActivatableViewModel { +public abstract class ChartViewModel extends ActivatableViewModel { public interface Listener { /** * @param fromDate Epoch date in millis for earliest data @@ -44,7 +44,7 @@ public interface Listener { private Predicate predicate = e -> true; - public ChartModel() { + public ChartViewModel() { } diff --git a/desktop/src/main/java/bisq/desktop/main/dao/economy/supply/SupplyView.java b/desktop/src/main/java/bisq/desktop/main/dao/economy/supply/SupplyView.java index b5118af6049..bcc5c33cdee 100644 --- a/desktop/src/main/java/bisq/desktop/main/dao/economy/supply/SupplyView.java +++ b/desktop/src/main/java/bisq/desktop/main/dao/economy/supply/SupplyView.java @@ -20,9 +20,9 @@ import bisq.desktop.common.view.ActivatableView; import bisq.desktop.common.view.FxmlView; import bisq.desktop.components.TitledGroupBg; -import bisq.desktop.components.chart.ChartModel; -import bisq.desktop.main.dao.economy.supply.daodata.DaoDataChartView; -import bisq.desktop.main.dao.economy.supply.daodata.DaoDataModel; +import bisq.desktop.components.chart.ChartViewModel; +import bisq.desktop.main.dao.economy.supply.daodata.DaoChartDataModel; +import bisq.desktop.main.dao.economy.supply.daodata.DaoChartView; import bisq.desktop.util.Layout; import bisq.core.dao.DaoFacade; @@ -49,11 +49,11 @@ import static bisq.desktop.util.FormBuilder.addTopLabelReadOnlyTextField; @FxmlView -public class SupplyView extends ActivatableView implements DaoStateListener, ChartModel.Listener { +public class SupplyView extends ActivatableView implements DaoStateListener, ChartViewModel.Listener { private final DaoFacade daoFacade; - private final DaoDataChartView daoDataChartView; + private final DaoChartView daoChartView; // Shared model between SupplyView and RevenueChartModel - private final DaoDataModel daoDataModel; + private final DaoChartDataModel daoChartDataModel; private final BsqFormatter bsqFormatter; private TextField genesisIssueAmountTextField, compRequestIssueAmountTextField, reimbursementAmountTextField, @@ -69,12 +69,12 @@ public class SupplyView extends ActivatableView implements DaoSt @Inject private SupplyView(DaoFacade daoFacade, - DaoDataChartView daoDataChartView, - DaoDataModel daoDataModel, + DaoChartView daoChartView, + DaoChartDataModel daoChartDataModel, BsqFormatter bsqFormatter) { this.daoFacade = daoFacade; - this.daoDataChartView = daoDataChartView; - this.daoDataModel = daoDataModel; + this.daoChartView = daoChartView; + this.daoChartDataModel = daoChartDataModel; this.bsqFormatter = bsqFormatter; } @@ -94,16 +94,16 @@ protected void activate() { updateWithBsqBlockChainData(); - daoDataChartView.activate(); - daoDataChartView.addListener(this); + daoChartView.activate(); + daoChartView.addListener(this); daoFacade.addBsqStateListener(this); } @Override protected void deactivate() { - daoDataChartView.removeListener(this); + daoChartView.removeListener(this); daoFacade.removeBsqStateListener(this); - daoDataChartView.deactivate(); + daoChartView.deactivate(); } @@ -134,11 +134,11 @@ public void onParseBlockCompleteAfterBatchProcessing(Block block) { private void createChart() { addTitledGroupBg(root, gridRow, 2, Res.get("dao.factsAndFigures.supply.issuedVsBurnt")); - daoDataChartView.initialize(); + daoChartView.initialize(); AnchorPane chartPane = new AnchorPane(); chartPane.getStyleClass().add("chart-pane"); - VBox chartContainer = daoDataChartView.getRoot(); + VBox chartContainer = daoChartView.getRoot(); AnchorPane.setTopAnchor(chartContainer, 15d); AnchorPane.setBottomAnchor(chartContainer, 10d); AnchorPane.setLeftAnchor(chartContainer, 25d); @@ -204,16 +204,16 @@ private void updateWithBsqBlockChainData() { private void updateEconomicsData() { // We use the supplyDataProvider to get the adjusted data with static historical data as well to use the same // monthly scoped data. - Coin issuedAmountFromCompRequests = Coin.valueOf(daoDataModel.getCompensationAmount(fromDate, getToDate())); + Coin issuedAmountFromCompRequests = Coin.valueOf(daoChartDataModel.getCompensationAmount(fromDate, getToDate())); compRequestIssueAmountTextField.setText(bsqFormatter.formatAmountWithGroupSeparatorAndCode(issuedAmountFromCompRequests)); - Coin issuedAmountFromReimbursementRequests = Coin.valueOf(daoDataModel.getReimbursementAmount(fromDate, getToDate())); + Coin issuedAmountFromReimbursementRequests = Coin.valueOf(daoChartDataModel.getReimbursementAmount(fromDate, getToDate())); reimbursementAmountTextField.setText(bsqFormatter.formatAmountWithGroupSeparatorAndCode(issuedAmountFromReimbursementRequests)); - Coin totalBurntTradeFee = Coin.valueOf(daoDataModel.getBsqTradeFeeAmount(fromDate, getToDate())); + Coin totalBurntTradeFee = Coin.valueOf(daoChartDataModel.getBsqTradeFeeAmount(fromDate, getToDate())); totalBurntBsqTradeFeeTextField.setText(bsqFormatter.formatAmountWithGroupSeparatorAndCode(totalBurntTradeFee)); - Coin totalProofOfBurnAmount = Coin.valueOf(daoDataModel.getProofOfBurnAmount(fromDate, getToDate())); + Coin totalProofOfBurnAmount = Coin.valueOf(daoChartDataModel.getProofOfBurnAmount(fromDate, getToDate())); totalProofOfBurnAmountTextField.setText(bsqFormatter.formatAmountWithGroupSeparatorAndCode(totalProofOfBurnAmount)); } diff --git a/desktop/src/main/java/bisq/desktop/main/dao/economy/supply/daodata/DaoDataModel.java b/desktop/src/main/java/bisq/desktop/main/dao/economy/supply/daodata/DaoChartDataModel.java similarity index 98% rename from desktop/src/main/java/bisq/desktop/main/dao/economy/supply/daodata/DaoDataModel.java rename to desktop/src/main/java/bisq/desktop/main/dao/economy/supply/daodata/DaoChartDataModel.java index 51baf657fa0..75c11d9c3fb 100644 --- a/desktop/src/main/java/bisq/desktop/main/dao/economy/supply/daodata/DaoDataModel.java +++ b/desktop/src/main/java/bisq/desktop/main/dao/economy/supply/daodata/DaoChartDataModel.java @@ -45,7 +45,7 @@ @Slf4j @Singleton -public class DaoDataModel { +public class DaoChartDataModel { private final DaoStateService daoStateService; private final Function blockTimeOfIssuanceFunction; private final TemporalAdjusterModel temporalAdjusterModel; @@ -55,7 +55,7 @@ public class DaoDataModel { /////////////////////////////////////////////////////////////////////////////////////////// @Inject - public DaoDataModel(DaoStateService daoStateService, TemporalAdjusterModel temporalAdjusterModel) { + public DaoChartDataModel(DaoStateService daoStateService, TemporalAdjusterModel temporalAdjusterModel) { super(); this.daoStateService = daoStateService; diff --git a/desktop/src/main/java/bisq/desktop/main/dao/economy/supply/daodata/DaoDataChartView.java b/desktop/src/main/java/bisq/desktop/main/dao/economy/supply/daodata/DaoChartView.java similarity index 98% rename from desktop/src/main/java/bisq/desktop/main/dao/economy/supply/daodata/DaoDataChartView.java rename to desktop/src/main/java/bisq/desktop/main/dao/economy/supply/daodata/DaoChartView.java index 75977cda65f..7a31215cbe0 100644 --- a/desktop/src/main/java/bisq/desktop/main/dao/economy/supply/daodata/DaoDataChartView.java +++ b/desktop/src/main/java/bisq/desktop/main/dao/economy/supply/daodata/DaoChartView.java @@ -51,7 +51,7 @@ import lombok.extern.slf4j.Slf4j; @Slf4j -public class DaoDataChartView extends ChartView { +public class DaoChartView extends ChartView { private static final DecimalFormat priceFormat = new DecimalFormat(",###"); private final BsqFormatter bsqFormatter; @@ -60,7 +60,7 @@ public class DaoDataChartView extends ChartView { private ListChangeListener nodeListChangeListener; @Inject - public DaoDataChartView(DaoDataChartModel model, BsqFormatter bsqFormatter) { + public DaoChartView(DaoChartViewModel model, BsqFormatter bsqFormatter) { super(model); this.bsqFormatter = bsqFormatter; diff --git a/desktop/src/main/java/bisq/desktop/main/dao/economy/supply/daodata/DaoDataChartModel.java b/desktop/src/main/java/bisq/desktop/main/dao/economy/supply/daodata/DaoChartViewModel.java similarity index 71% rename from desktop/src/main/java/bisq/desktop/main/dao/economy/supply/daodata/DaoDataChartModel.java rename to desktop/src/main/java/bisq/desktop/main/dao/economy/supply/daodata/DaoChartViewModel.java index 2c0fb1f49bf..f5e85bea0b6 100644 --- a/desktop/src/main/java/bisq/desktop/main/dao/economy/supply/daodata/DaoDataChartModel.java +++ b/desktop/src/main/java/bisq/desktop/main/dao/economy/supply/daodata/DaoChartViewModel.java @@ -17,7 +17,7 @@ package bisq.desktop.main.dao.economy.supply.daodata; -import bisq.desktop.components.chart.ChartModel; +import bisq.desktop.components.chart.ChartViewModel; import bisq.core.dao.state.DaoStateService; @@ -38,59 +38,59 @@ import lombok.extern.slf4j.Slf4j; @Slf4j -public class DaoDataChartModel extends ChartModel { +public class DaoChartViewModel extends ChartViewModel { private final DaoStateService daoStateService; - private final DaoDataModel daoDataModel; + private final DaoChartDataModel daoChartDataModel; /////////////////////////////////////////////////////////////////////////////////////////// // Constructor /////////////////////////////////////////////////////////////////////////////////////////// @Inject - public DaoDataChartModel(DaoStateService daoStateService, DaoDataModel daoDataModel) { + public DaoChartViewModel(DaoStateService daoStateService, DaoChartDataModel daoChartDataModel) { super(); this.daoStateService = daoStateService; - this.daoDataModel = daoDataModel; + this.daoChartDataModel = daoChartDataModel; } @Override protected void applyTemporalAdjuster(TemporalAdjuster temporalAdjuster) { - daoDataModel.setTemporalAdjuster(temporalAdjuster); + daoChartDataModel.setTemporalAdjuster(temporalAdjuster); } @Override protected TemporalAdjuster getTemporalAdjuster() { - return daoDataModel.getTemporalAdjuster(); + return daoChartDataModel.getTemporalAdjuster(); } List> getBsqTradeFeeChartData(Predicate predicate) { - return toChartData(daoDataModel.getBurnedBsqByMonth(daoStateService.getTradeFeeTxs(), predicate)); + return toChartData(daoChartDataModel.getBurnedBsqByMonth(daoStateService.getTradeFeeTxs(), predicate)); } List> getCompensationChartData(Predicate predicate) { - return toChartData(daoDataModel.getMergedCompensationMap(predicate)); + return toChartData(daoChartDataModel.getMergedCompensationMap(predicate)); } List> getProofOfBurnChartData(Predicate predicate) { - return toChartData(daoDataModel.getBurnedBsqByMonth(daoStateService.getProofOfBurnTxs(), predicate)); + return toChartData(daoChartDataModel.getBurnedBsqByMonth(daoStateService.getProofOfBurnTxs(), predicate)); } List> getReimbursementChartData(Predicate predicate) { - return toChartData(daoDataModel.getMergedReimbursementMap(predicate)); + return toChartData(daoChartDataModel.getMergedReimbursementMap(predicate)); } List> getTotalIssuedChartData(Predicate predicate) { - Map compensationMap = daoDataModel.getMergedCompensationMap(predicate); - Map reimbursementMap = daoDataModel.getMergedReimbursementMap(predicate); - Map sum = daoDataModel.getMergedMap(compensationMap, reimbursementMap, Long::sum); + Map compensationMap = daoChartDataModel.getMergedCompensationMap(predicate); + Map reimbursementMap = daoChartDataModel.getMergedReimbursementMap(predicate); + Map sum = daoChartDataModel.getMergedMap(compensationMap, reimbursementMap, Long::sum); return toChartData(sum); } List> getTotalBurnedChartData(Predicate predicate) { - Map tradeFee = daoDataModel.getBurnedBsqByMonth(daoStateService.getTradeFeeTxs(), predicate); - Map proofOfBurn = daoDataModel.getBurnedBsqByMonth(daoStateService.getProofOfBurnTxs(), predicate); - Map sum = daoDataModel.getMergedMap(tradeFee, proofOfBurn, Long::sum); + Map tradeFee = daoChartDataModel.getBurnedBsqByMonth(daoStateService.getTradeFeeTxs(), predicate); + Map proofOfBurn = daoChartDataModel.getBurnedBsqByMonth(daoStateService.getProofOfBurnTxs(), predicate); + Map sum = daoChartDataModel.getMergedMap(tradeFee, proofOfBurn, Long::sum); return toChartData(sum); } @@ -104,7 +104,7 @@ void initBounds(List> tradeFeeChartData, } long toTimeInterval(Instant ofEpochSecond) { - return daoDataModel.toTimeInterval(ofEpochSecond); + return daoChartDataModel.toTimeInterval(ofEpochSecond); } /////////////////////////////////////////////////////////////////////////////////////////// From d2f43373fd2d11b4dd226f32b66483bf4b402871 Mon Sep 17 00:00:00 2001 From: chimp1984 Date: Mon, 8 Feb 2021 17:49:02 -0500 Subject: [PATCH 13/21] Add comments --- .../java/bisq/core/trade/statistics/TradeStatistics3.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/core/src/main/java/bisq/core/trade/statistics/TradeStatistics3.java b/core/src/main/java/bisq/core/trade/statistics/TradeStatistics3.java index f4081939c74..a4dd770c47b 100644 --- a/core/src/main/java/bisq/core/trade/statistics/TradeStatistics3.java +++ b/core/src/main/java/bisq/core/trade/statistics/TradeStatistics3.java @@ -160,7 +160,7 @@ private enum PaymentMethodMapper { @Getter private final long price; @Getter - private final long amount; + private final long amount; // BTC amount private final String paymentMethod; // As only seller is publishing it is the sellers trade date private final long date; @@ -193,7 +193,7 @@ private enum PaymentMethodMapper { private transient final Date dateObj; @JsonExclude - private transient Volume volume = null; + private transient Volume volume = null; // Fiat or altcoin volume @JsonExclude private transient LocalDateTime localDateTime; From f421411bba81e26d21f983fef350748b61b27052 Mon Sep 17 00:00:00 2001 From: chimp1984 Date: Mon, 8 Feb 2021 17:51:55 -0500 Subject: [PATCH 14/21] Add price and volume charts --- .../resources/i18n/displayStrings.properties | 18 +- .../components/chart/ChartDataModel.java | 94 +++ .../desktop/components/chart/ChartView.java | 739 +++++++++++------- .../components/chart/ChartViewModel.java | 225 ++++-- .../chart/TemporalAdjusterModel.java | 9 +- .../economy/dashboard/BsqDashboardView.java | 231 ++---- .../dashboard/price/PriceChartDataModel.java | 194 +++++ .../dashboard/price/PriceChartView.java | 137 ++++ .../dashboard/price/PriceChartViewModel.java | 105 +++ .../volume/VolumeChartDataModel.java | 141 ++++ .../dashboard/volume/VolumeChartView.java | 125 +++ .../volume/VolumeChartViewModel.java | 94 +++ .../main/dao/economy/supply/SupplyView.java | 57 +- .../{daodata => dao}/DaoChartDataModel.java | 212 ++--- .../dao/economy/supply/dao/DaoChartView.java | 173 ++++ .../economy/supply/dao/DaoChartViewModel.java | 125 +++ .../economy/supply/daodata/DaoChartView.java | 292 ------- .../supply/daodata/DaoChartViewModel.java | 128 --- .../src/main/java/bisq/desktop/theme-dark.css | 2 +- .../main/java/bisq/desktop/theme-light.css | 2 +- 20 files changed, 2056 insertions(+), 1047 deletions(-) create mode 100644 desktop/src/main/java/bisq/desktop/components/chart/ChartDataModel.java create mode 100644 desktop/src/main/java/bisq/desktop/main/dao/economy/dashboard/price/PriceChartDataModel.java create mode 100644 desktop/src/main/java/bisq/desktop/main/dao/economy/dashboard/price/PriceChartView.java create mode 100644 desktop/src/main/java/bisq/desktop/main/dao/economy/dashboard/price/PriceChartViewModel.java create mode 100644 desktop/src/main/java/bisq/desktop/main/dao/economy/dashboard/volume/VolumeChartDataModel.java create mode 100644 desktop/src/main/java/bisq/desktop/main/dao/economy/dashboard/volume/VolumeChartView.java create mode 100644 desktop/src/main/java/bisq/desktop/main/dao/economy/dashboard/volume/VolumeChartViewModel.java rename desktop/src/main/java/bisq/desktop/main/dao/economy/supply/{daodata => dao}/DaoChartDataModel.java (53%) create mode 100644 desktop/src/main/java/bisq/desktop/main/dao/economy/supply/dao/DaoChartView.java create mode 100644 desktop/src/main/java/bisq/desktop/main/dao/economy/supply/dao/DaoChartViewModel.java delete mode 100644 desktop/src/main/java/bisq/desktop/main/dao/economy/supply/daodata/DaoChartView.java delete mode 100644 desktop/src/main/java/bisq/desktop/main/dao/economy/supply/daodata/DaoChartViewModel.java diff --git a/core/src/main/resources/i18n/displayStrings.properties b/core/src/main/resources/i18n/displayStrings.properties index 82210faefae..f40f3d628a2 100644 --- a/core/src/main/resources/i18n/displayStrings.properties +++ b/core/src/main/resources/i18n/displayStrings.properties @@ -2430,9 +2430,9 @@ dao.factsAndFigures.menuItem.transactions=BSQ Transactions dao.factsAndFigures.dashboard.avgPrice90=90 days average BSQ/BTC trade price dao.factsAndFigures.dashboard.avgPrice30=30 days average BSQ/BTC trade price -dao.factsAndFigures.dashboard.avgUSDPrice90=90 days volume weighted average USD/BSQ price -dao.factsAndFigures.dashboard.avgUSDPrice30=30 days volume weighted average USD/BSQ price -dao.factsAndFigures.dashboard.marketCap=Market capitalisation (based on 30 days average USD/BSQ price) +dao.factsAndFigures.dashboard.avgUSDPrice90=90 days volume weighted average BSQ/USD price +dao.factsAndFigures.dashboard.avgUSDPrice30=30 days volume weighted average BSQ/USD price +dao.factsAndFigures.dashboard.marketCap=Market capitalisation (based on 30 days average BSQ/USD price) dao.factsAndFigures.dashboard.availableAmount=Total available BSQ dao.factsAndFigures.supply.issuedVsBurnt=BSQ issued v. BSQ burnt @@ -2445,11 +2445,17 @@ dao.factsAndFigures.supply.compRequestIssueAmount=BSQ issued for compensation re dao.factsAndFigures.supply.reimbursementAmount=BSQ issued for reimbursement requests dao.factsAndFigures.supply.totalIssued=Total issues BSQ dao.factsAndFigures.supply.totalBurned=Total burned BSQ - dao.factsAndFigures.supply.chart.tradeFee.toolTip={0}\n{1} - dao.factsAndFigures.supply.burnt=BSQ burnt +dao.factsAndFigures.supply.priceChat=BSQ price +dao.factsAndFigures.supply.volumeChat=Trade volume +dao.factsAndFigures.supply.tradeVolumeInUsd=Trade volume in USD +dao.factsAndFigures.supply.tradeVolumeInBtc=Trade volume in BTC +dao.factsAndFigures.supply.bsqUsdPrice=BSQ/USD price +dao.factsAndFigures.supply.bsqBtcPrice=BSQ/BTC price +dao.factsAndFigures.supply.btcUsdPrice=BTC/USD price + dao.factsAndFigures.supply.locked=Global state of locked BSQ dao.factsAndFigures.supply.totalLockedUpAmount=Locked up in bonds dao.factsAndFigures.supply.totalUnlockingAmount=Unlocking BSQ from bonds @@ -2471,6 +2477,8 @@ dao.factsAndFigures.transactions.burntTx=No. of all fee payments transactions dao.factsAndFigures.transactions.invalidTx=No. of all invalid transactions dao.factsAndFigures.transactions.irregularTx=No. of all irregular transactions + + #################################################################### # Windows #################################################################### diff --git a/desktop/src/main/java/bisq/desktop/components/chart/ChartDataModel.java b/desktop/src/main/java/bisq/desktop/components/chart/ChartDataModel.java new file mode 100644 index 00000000000..b8ab438873e --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/components/chart/ChartDataModel.java @@ -0,0 +1,94 @@ +/* + * This file is part of Bisq. + * + * Bisq is free software: you can redistribute it and/or modify it + * under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at + * your option) any later version. + * + * Bisq is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public + * License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Bisq. If not, see . + */ + +package bisq.desktop.components.chart; + +import bisq.desktop.common.model.ActivatableDataModel; + +import java.time.Instant; +import java.time.temporal.TemporalAdjuster; + +import java.util.Map; +import java.util.function.BinaryOperator; +import java.util.function.Predicate; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +public abstract class ChartDataModel extends ActivatableDataModel { + protected final TemporalAdjusterModel temporalAdjusterModel = new TemporalAdjusterModel(); + protected Predicate dateFilter = e -> true; + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Constructor + /////////////////////////////////////////////////////////////////////////////////////////// + + public ChartDataModel() { + super(); + } + + @Override + public void activate() { + dateFilter = e -> true; + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // TemporalAdjusterModel delegates + /////////////////////////////////////////////////////////////////////////////////////////// + + void setTemporalAdjuster(TemporalAdjuster temporalAdjuster) { + temporalAdjusterModel.setTemporalAdjuster(temporalAdjuster); + } + + TemporalAdjuster getTemporalAdjuster() { + return temporalAdjusterModel.getTemporalAdjuster(); + } + + public long toTimeInterval(Instant instant) { + return temporalAdjusterModel.toTimeInterval(instant); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Date filter predicate + /////////////////////////////////////////////////////////////////////////////////////////// + + public Predicate getDateFilter() { + return dateFilter; + } + + void setDateFilter(double from, double to) { + dateFilter = value -> value >= from && value <= to; + } + + /////////////////////////////////////////////////////////////////////////////////////////// + // Data + /////////////////////////////////////////////////////////////////////////////////////////// + + protected abstract void invalidateCache(); + + protected Map getMergedMap(Map map1, + Map map2, + BinaryOperator mergeFunction) { + return Stream.concat(map1.entrySet().stream(), + map2.entrySet().stream()) + .collect(Collectors.toMap(Map.Entry::getKey, + Map.Entry::getValue, + mergeFunction)); + } +} diff --git a/desktop/src/main/java/bisq/desktop/components/chart/ChartView.java b/desktop/src/main/java/bisq/desktop/components/chart/ChartView.java index 3812ca45399..40484e71655 100644 --- a/desktop/src/main/java/bisq/desktop/components/chart/ChartView.java +++ b/desktop/src/main/java/bisq/desktop/components/chart/ChartView.java @@ -17,12 +17,13 @@ package bisq.desktop.components.chart; -import bisq.desktop.common.view.ActivatableView; +import bisq.desktop.common.view.ActivatableViewAndModel; import bisq.desktop.components.AutoTooltipSlideToggleButton; import bisq.desktop.components.AutoTooltipToggleButton; import bisq.core.locale.Res; +import bisq.common.UserThread; import bisq.common.util.Tuple2; import javafx.scene.Node; @@ -43,178 +44,242 @@ import javafx.scene.layout.Priority; import javafx.scene.layout.Region; import javafx.scene.layout.VBox; +import javafx.scene.text.Text; import javafx.geometry.Insets; import javafx.geometry.Pos; +import javafx.geometry.Side; import javafx.beans.value.ChangeListener; +import javafx.collections.ListChangeListener; +import javafx.collections.ObservableList; + import javafx.util.StringConverter; import java.time.temporal.TemporalAdjuster; -import java.text.DateFormat; -import java.text.SimpleDateFormat; - import java.util.ArrayList; import java.util.Collection; -import java.util.Date; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Optional; import java.util.Set; -import java.util.function.Predicate; import java.util.stream.Stream; +import lombok.Setter; import lombok.extern.slf4j.Slf4j; +import javax.annotation.Nullable; + import static bisq.desktop.util.FormBuilder.getTopLabelWithVBox; @Slf4j -public abstract class ChartView extends ActivatableView { - private final Pane center; - private final SplitPane splitPane; - protected final NumberAxis xAxis; - protected final NumberAxis yAxis; - protected final XYChart chart; +public abstract class ChartView> extends ActivatableViewAndModel { + private Pane center; + private SplitPane timelineNavigation; + protected NumberAxis xAxis; + protected NumberAxis yAxis; + protected LineChart chart; + private HBox timelineLabels; + private final ToggleGroup timeIntervalToggleGroup = new ToggleGroup(); + + protected final Set> activeSeries = new HashSet<>(); protected final Map seriesIndexMap = new HashMap<>(); - protected final Map toggleBySeriesName = new HashMap<>(); - private final HBox timelineLabels; + protected final Map legendToggleBySeriesName = new HashMap<>(); + private final List dividerNodes = new ArrayList<>(); - private final Double[] dividerPositions = new Double[]{0d, 1d}; - private final ToggleGroup timeUnitToggleGroup = new ToggleGroup(); - protected final Set activeSeries = new HashSet<>(); - private boolean pressed; - private double x; + private ChangeListener widthListener; + private ChangeListener timeIntervalChangeListener; + private ListChangeListener nodeListChangeListener; private int maxSeriesSize; - private ChangeListener timeUnitChangeListener; - protected String dateFormatPatters = "dd MMM\nyyyy"; + private boolean pressed; + private double x; + + @Setter + protected boolean isRadioButtonBehaviour; + @Setter + private int maxDataPointsForShowingSymbols = 100; + private HBox legendBox2; + private ChangeListener yAxisWidthListener; + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Constructor + /////////////////////////////////////////////////////////////////////////////////////////// public ChartView(T model) { super(model); root = new VBox(); + } + + /////////////////////////////////////////////////////////////////////////////////////////// + // Lifecycle + /////////////////////////////////////////////////////////////////////////////////////////// - // time units + @Override + public void initialize() { + // We need to call prepareInitialize as we are not using FXMLLoader + prepareInitialize(); + + maxSeriesSize = 0; + pressed = false; + x = 0; + + // Series + createSeries(); + + // Time interval Pane timeIntervalBox = getTimeIntervalBox(); // chart xAxis = getXAxis(); yAxis = getYAxis(); chart = getChart(); - initSeries(); - addActiveSeries(); - HBox legendBox1 = getLegendBox(getSeriesForLegend1()); + + // Timeline navigation + addTimelineNavigation(); + + // Legend + HBox legendBox1 = initLegendsAndGetLegendBox(getSeriesForLegend1()); + Collection> seriesForLegend2 = getSeriesForLegend2(); - HBox legendBox2 = null; if (seriesForLegend2 != null && !seriesForLegend2.isEmpty()) { - legendBox2 = getLegendBox(seriesForLegend2); + legendBox2 = initLegendsAndGetLegendBox(seriesForLegend2); } - // Time navigation - Pane left = new Pane(); - center = new Pane(); - Pane right = new Pane(); - splitPane = new SplitPane(); - splitPane.getItems().addAll(left, center, right); - timelineLabels = new HBox(); + // Set active series/legends + defineAndAddActiveSeries(); + + // Put all together + VBox timelineNavigationBox = new VBox(); + + double paddingLeft = 15; + double paddingRight = 89; + // Y-axis width depends on data so we register a listener to get correct value + yAxisWidthListener = (observable, oldValue, newValue) -> { + double width = newValue.doubleValue(); + if (width > 0) { + double paddingRight1 = width + 14; + VBox.setMargin(timelineNavigation, new Insets(0, paddingRight1, 0, paddingLeft)); + VBox.setMargin(timelineLabels, new Insets(0, paddingRight1, 0, paddingLeft)); + VBox.setMargin(legendBox1, new Insets(10, paddingRight1, 0, paddingLeft)); + if (legendBox2 != null) { + VBox.setMargin(legendBox2, new Insets(-20, paddingRight1, 0, paddingLeft)); + } - // Container - VBox box = new VBox(); - int paddingRight = 89; - int paddingLeft = 15; - VBox.setMargin(splitPane, new Insets(0, paddingRight, 0, paddingLeft)); + if (model.getDividerPositions()[0] == 0 && model.getDividerPositions()[1] == 1) { + resetTimeNavigation(); + } + } + }; + + VBox.setMargin(timelineNavigation, new Insets(0, paddingRight, 0, paddingLeft)); VBox.setMargin(timelineLabels, new Insets(0, paddingRight, 0, paddingLeft)); VBox.setMargin(legendBox1, new Insets(10, paddingRight, 0, paddingLeft)); - box.getChildren().addAll(splitPane, timelineLabels, legendBox1); + timelineNavigationBox.getChildren().addAll(timelineNavigation, timelineLabels, legendBox1); if (legendBox2 != null) { VBox.setMargin(legendBox2, new Insets(-20, paddingRight, 0, paddingLeft)); - box.getChildren().add(legendBox2); + timelineNavigationBox.getChildren().add(legendBox2); } - root.getChildren().addAll(timeIntervalBox, chart, box); - } - - protected abstract void addActiveSeries(); - - /////////////////////////////////////////////////////////////////////////////////////////// - // Lifecycle - /////////////////////////////////////////////////////////////////////////////////////////// - - @Override - public void initialize() { - center.setId("chart-navigation-center-pane"); - - splitPane.setMinHeight(30); - splitPane.setDividerPosition(0, dividerPositions[0]); - splitPane.setDividerPosition(1, dividerPositions[1]); + root.getChildren().addAll(timeIntervalBox, chart, timelineNavigationBox); + // Listeners widthListener = (observable, oldValue, newValue) -> { - splitPane.setDividerPosition(0, dividerPositions[0]); - splitPane.setDividerPosition(1, dividerPositions[1]); + timelineNavigation.setDividerPosition(0, model.getDividerPositions()[0]); + timelineNavigation.setDividerPosition(1, model.getDividerPositions()[1]); }; - timeUnitChangeListener = (observable, oldValue, newValue) -> { - TemporalAdjusterModel.Interval interval = (TemporalAdjusterModel.Interval) newValue.getUserData(); - applyTemporalAdjuster(interval.getAdjuster()); + timeIntervalChangeListener = (observable, oldValue, newValue) -> { + if (newValue != null) { + onTimeIntervalChanged(newValue); + } + }; + + nodeListChangeListener = c -> { + while (c.next()) { + if (c.wasAdded()) { + c.getAddedSubList().stream() + .filter(node -> node instanceof Text) + .forEach(node -> node.getStyleClass().add("axis-tick-mark-text-node")); + } + } }; } @Override public void activate() { + timelineNavigation.setDividerPositions(model.getDividerPositions()[0], model.getDividerPositions()[1]); + + TemporalAdjuster temporalAdjuster = model.getTemporalAdjuster(); + applyTemporalAdjuster(temporalAdjuster); + findTimeIntervalToggleByTemporalAdjuster(temporalAdjuster).ifPresent(timeIntervalToggleGroup::selectToggle); + + defineAndAddActiveSeries(); + applyData(); + initBoundsForTimelineNavigation(); + + updateChartAfterDataChange(); + // Need delay to next render frame + UserThread.execute(this::applyTimeLineNavigationLabels); + + // Apply listeners and handlers root.widthProperty().addListener(widthListener); - timeUnitToggleGroup.selectedToggleProperty().addListener(timeUnitChangeListener); + xAxis.getChildrenUnmodifiable().addListener(nodeListChangeListener); + yAxis.widthProperty().addListener(yAxisWidthListener); + timeIntervalToggleGroup.selectedToggleProperty().addListener(timeIntervalChangeListener); - splitPane.setOnMousePressed(this::onMousePressedSplitPane); - splitPane.setOnMouseDragged(this::onMouseDragged); + timelineNavigation.setOnMousePressed(this::onMousePressedSplitPane); + timelineNavigation.setOnMouseDragged(this::onMouseDragged); center.setOnMousePressed(this::onMousePressedCenter); center.setOnMouseReleased(this::onMouseReleasedCenter); - initData(); - initDividerMouseHandlers(); - // Need to get called again here as otherwise styles are not applied correctly - applySeriesStyles(); - - TemporalAdjuster temporalAdjuster = model.getTemporalAdjuster(); - applyTemporalAdjuster(temporalAdjuster); - findToggleByTemporalAdjuster(temporalAdjuster).ifPresent(timeUnitToggleGroup::selectToggle); + addLegendToggleActionHandlers(getSeriesForLegend1()); + addLegendToggleActionHandlers(getSeriesForLegend2()); + addActionHandlersToDividers(); } @Override public void deactivate() { root.widthProperty().removeListener(widthListener); - timeUnitToggleGroup.selectedToggleProperty().removeListener(timeUnitChangeListener); - splitPane.setOnMousePressed(null); - splitPane.setOnMouseDragged(null); + xAxis.getChildrenUnmodifiable().removeListener(nodeListChangeListener); + yAxis.widthProperty().removeListener(yAxisWidthListener); + timeIntervalToggleGroup.selectedToggleProperty().removeListener(timeIntervalChangeListener); + + timelineNavigation.setOnMousePressed(null); + timelineNavigation.setOnMouseDragged(null); center.setOnMousePressed(null); center.setOnMouseReleased(null); - dividerNodes.forEach(node -> node.setOnMouseReleased(null)); - } + removeLegendToggleActionHandlers(getSeriesForLegend1()); + removeLegendToggleActionHandlers(getSeriesForLegend2()); + removeActionHandlersToDividers(); - public void addListener(ChartViewModel.Listener listener) { - model.addListener(listener); + // clear data, reset states. We keep timeInterval state though + activeSeries.clear(); + chart.getData().clear(); + legendToggleBySeriesName.values().forEach(e -> e.setSelected(false)); + model.invalidateCache(); } - public void removeListener(ChartViewModel.Listener listener) { - model.removeListener(listener); - } /////////////////////////////////////////////////////////////////////////////////////////// - // Customisations + // TimeInterval/TemporalAdjuster /////////////////////////////////////////////////////////////////////////////////////////// protected Pane getTimeIntervalBox() { - ToggleButton year = getToggleButton(Res.get("time.year"), TemporalAdjusterModel.Interval.YEAR, - timeUnitToggleGroup, "toggle-left"); - ToggleButton month = getToggleButton(Res.get("time.month"), TemporalAdjusterModel.Interval.MONTH, - timeUnitToggleGroup, "toggle-center"); - ToggleButton week = getToggleButton(Res.get("time.week"), TemporalAdjusterModel.Interval.WEEK, - timeUnitToggleGroup, "toggle-center"); - ToggleButton day = getToggleButton(Res.get("time.day"), TemporalAdjusterModel.Interval.DAY, - timeUnitToggleGroup, "toggle-center"); + ToggleButton year = getTimeIntervalToggleButton(Res.get("time.year"), TemporalAdjusterModel.Interval.YEAR, + timeIntervalToggleGroup, "toggle-left"); + ToggleButton month = getTimeIntervalToggleButton(Res.get("time.month"), TemporalAdjusterModel.Interval.MONTH, + timeIntervalToggleGroup, "toggle-center"); + ToggleButton week = getTimeIntervalToggleButton(Res.get("time.week"), TemporalAdjusterModel.Interval.WEEK, + timeIntervalToggleGroup, "toggle-center"); + ToggleButton day = getTimeIntervalToggleButton(Res.get("time.day"), TemporalAdjusterModel.Interval.DAY, + timeIntervalToggleGroup, "toggle-center"); HBox toggleBox = new HBox(); toggleBox.setSpacing(0); @@ -229,10 +294,10 @@ protected Pane getTimeIntervalBox() { return pane; } - protected ToggleButton getToggleButton(String label, - TemporalAdjusterModel.Interval interval, - ToggleGroup toggleGroup, - String style) { + private ToggleButton getTimeIntervalToggleButton(String label, + TemporalAdjusterModel.Interval interval, + ToggleGroup toggleGroup, + String style) { ToggleButton toggleButton = new AutoTooltipToggleButton(label); toggleButton.setUserData(interval); toggleButton.setToggleGroup(toggleGroup); @@ -240,45 +305,64 @@ protected ToggleButton getToggleButton(String label, return toggleButton; } + protected void applyTemporalAdjuster(TemporalAdjuster temporalAdjuster) { + model.applyTemporalAdjuster(temporalAdjuster); + findTimeIntervalToggleByTemporalAdjuster(temporalAdjuster) + .map(e -> (TemporalAdjusterModel.Interval) e.getUserData()) + .ifPresent(model::setDateFormatPattern); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Chart + /////////////////////////////////////////////////////////////////////////////////////////// + protected NumberAxis getXAxis() { - NumberAxis xAxis; - xAxis = new NumberAxis(); + NumberAxis xAxis = new NumberAxis(); xAxis.setForceZeroInRange(false); - xAxis.setTickLabelFormatter(getTimeAxisStringConverter()); - xAxis.setAutoRanging(true); + xAxis.setTickLabelFormatter(model.getTimeAxisStringConverter()); return xAxis; } protected NumberAxis getYAxis() { - NumberAxis yAxis; - yAxis = new NumberAxis(); - yAxis.setForceZeroInRange(false); - yAxis.setTickLabelFormatter(getYAxisStringConverter()); + NumberAxis yAxis = new NumberAxis(); + yAxis.setForceZeroInRange(true); + yAxis.setSide(Side.RIGHT); + StringConverter yAxisStringConverter = model.getYAxisStringConverter(); + yAxis.setTickLabelFormatter(yAxisStringConverter); return yAxis; } - protected XYChart getChart() { + // Add implementation if update of the y axis is required at series change + protected void onSetYAxisFormatter(XYChart.Series series) { + } + + protected LineChart getChart() { LineChart chart = new LineChart<>(xAxis, yAxis); - chart.setLegendVisible(false); chart.setAnimated(false); + chart.setCreateSymbols(true); + chart.setLegendVisible(false); + chart.setId("charts-dao"); return chart; } - protected abstract void initSeries(); - protected HBox getLegendBox(Collection> data) { + /////////////////////////////////////////////////////////////////////////////////////////// + // Legend + /////////////////////////////////////////////////////////////////////////////////////////// + + protected HBox initLegendsAndGetLegendBox(Collection> collection) { HBox hBox = new HBox(); hBox.setSpacing(10); - data.forEach(series -> { + collection.forEach(series -> { AutoTooltipSlideToggleButton toggle = new AutoTooltipSlideToggleButton(); toggle.setMinWidth(200); toggle.setAlignment(Pos.TOP_LEFT); - String seriesName = getSeriesId(series); - toggleBySeriesName.put(seriesName, toggle); - toggle.setText(seriesName); - toggle.setId("charts-legend-toggle" + seriesIndexMap.get(seriesName)); - toggle.setSelected(true); - toggle.setOnAction(e -> onSelectLegendToggle(series, toggle.isSelected())); + String seriesId = getSeriesId(series); + legendToggleBySeriesName.put(seriesId, toggle); + toggle.setText(seriesId); + toggle.setId("charts-legend-toggle" + seriesIndexMap.get(seriesId)); + toggle.setSelected(false); hBox.getChildren().add(toggle); }); Region spacer = new Region(); @@ -287,114 +371,38 @@ protected HBox getLegendBox(Collection> data) { return hBox; } - protected abstract Collection> getSeriesForLegend1(); - - protected abstract Collection> getSeriesForLegend2(); - - private void onSelectLegendToggle(XYChart.Series series, boolean isSelected) { - if (isSelected) { - activateSeries(series); - } else { - chart.getData().remove(series); - activeSeries.remove(getSeriesId(series)); + private void addLegendToggleActionHandlers(@Nullable Collection> collection) { + if (collection != null) { + collection.forEach(series -> + legendToggleBySeriesName.get(getSeriesId(series)).setOnAction(e -> onSelectLegendToggle(series))); } - applySeriesStyles(); - applyTooltip(); - } - - protected void activateSeries(XYChart.Series series) { - chart.getData().add(series); - activeSeries.add(getSeriesId(series)); - } - - protected void hideSeries(XYChart.Series series) { - toggleBySeriesName.get(getSeriesId(series)).setSelected(false); - onSelectLegendToggle(series, false); - } - - protected StringConverter getTimeAxisStringConverter() { - return new StringConverter<>() { - @Override - public String toString(Number value) { - DateFormat format = new SimpleDateFormat(dateFormatPatters); - Date date = new Date(Math.round(value.doubleValue()) * 1000); - return format.format(date); - } - - @Override - public Number fromString(String string) { - return null; - } - }; - } - - protected StringConverter getYAxisStringConverter() { - return new StringConverter<>() { - @Override - public String toString(Number value) { - return String.valueOf(value); - } - - @Override - public Number fromString(String string) { - return null; - } - }; } - protected void applyTemporalAdjuster(TemporalAdjuster temporalAdjuster) { - model.applyTemporalAdjuster(temporalAdjuster); - findToggleByTemporalAdjuster(temporalAdjuster) - .map(e -> (TemporalAdjusterModel.Interval) e.getUserData()) - .ifPresent(this::setDateFormatPatters); - - updateData(model.getPredicate()); - } - - private void setDateFormatPatters(TemporalAdjusterModel.Interval interval) { - switch (interval) { - case YEAR: - dateFormatPatters = "yyyy"; - break; - case MONTH: - dateFormatPatters = "MMM\nyyyy"; - break; - default: - dateFormatPatters = "MMM dd\nyyyy"; - break; + private void removeLegendToggleActionHandlers(@Nullable Collection> collection) { + if (collection != null) { + collection.forEach(series -> + legendToggleBySeriesName.get(getSeriesId(series)).setOnAction(null)); } - } - protected abstract void initData(); - - protected abstract void updateData(Predicate predicate); - - protected void applyTooltip() { - chart.getData().forEach(series -> { - series.getData().forEach(data -> { - Node node = data.getNode(); - if (node == null) { - return; - } - String xValue = getTooltipDateConverter(data.getXValue()); - String yValue = getYAxisStringConverter().toString(data.getYValue()); - Tooltip.install(node, new Tooltip(Res.get("dao.factsAndFigures.supply.chart.tradeFee.toolTip", yValue, xValue))); - }); - }); - } - protected String getTooltipDateConverter(Number date) { - return getTimeAxisStringConverter().toString(date).replace("\n", " "); - } + /////////////////////////////////////////////////////////////////////////////////////////// + // Timeline navigation + /////////////////////////////////////////////////////////////////////////////////////////// - protected String getTooltipValueConverter(Number value) { - return getYAxisStringConverter().toString(value); + private void addTimelineNavigation() { + Pane left = new Pane(); + center = new Pane(); + center.setId("chart-navigation-center-pane"); + Pane right = new Pane(); + timelineNavigation = new SplitPane(); + timelineNavigation.setMinHeight(30); + timelineNavigation.getItems().addAll(left, center, right); + timelineLabels = new HBox(); } - // Only called once when initial data are applied. We want the min. and max. values so we have the max. scale for - // navigation. - protected void setTimeLineLabels() { + // After initial chart data are created we apply the text from the x-axis ticks to our timeline navigation. + protected void applyTimeLineNavigationLabels() { timelineLabels.getChildren().clear(); int size = xAxis.getTickMarks().size(); for (int i = 0; i < size; i++) { @@ -407,6 +415,7 @@ protected void setTimeLineLabels() { xValueString = String.valueOf(xValue); } Label label = new Label(xValueString); + label.setMinHeight(30); label.setId("chart-navigation-label"); Region spacer = new Region(); HBox.setHgrow(spacer, Priority.ALWAYS); @@ -419,9 +428,219 @@ protected void setTimeLineLabels() { } } + private void resetTimeNavigation() { + timelineNavigation.setDividerPositions(0d, 1d); + model.onTimelineNavigationChanged(0, 1); + } + + + private void onMouseDragged(MouseEvent e) { + if (pressed) { + double newX = e.getX(); + double width = timelineNavigation.getWidth(); + double relativeDelta = (x - newX) / width; + double leftPos = timelineNavigation.getDividerPositions()[0] - relativeDelta; + double rightPos = timelineNavigation.getDividerPositions()[1] - relativeDelta; + + // Model might limit application of new values if we hit a boundary + model.onTimelineMouseDrag(leftPos, rightPos); + timelineNavigation.setDividerPositions(model.getDividerPositions()[0], model.getDividerPositions()[1]); + x = newX; + } + } + + private void onMouseReleasedCenter(MouseEvent e) { + pressed = false; + onTimelineChanged(); + } + + private void onMousePressedSplitPane(MouseEvent e) { + x = e.getX(); + } + + private void onMousePressedCenter(MouseEvent e) { + pressed = true; + } + + private void addActionHandlersToDividers() { + // No API access to dividers ;-( only via css lookup hack (https://stackoverflow.com/questions/40707295/how-to-add-listener-to-divider-position?rq=1) + // Need to be done after added to scene and call requestLayout and applyCss. We keep it in a list atm + // and set action handler in activate. + timelineNavigation.requestLayout(); + timelineNavigation.applyCss(); + for (Node node : timelineNavigation.lookupAll(".split-pane-divider")) { + dividerNodes.add(node); + node.setOnMouseReleased(e -> onTimelineChanged()); + } + } + + private void removeActionHandlersToDividers() { + dividerNodes.forEach(node -> node.setOnMouseReleased(null)); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Series + /////////////////////////////////////////////////////////////////////////////////////////// + + protected abstract void createSeries(); + + protected abstract Collection> getSeriesForLegend1(); + + // If a second legend is used this has to be overridden + protected Collection> getSeriesForLegend2() { + return null; + } + + protected abstract void defineAndAddActiveSeries(); + + protected void activateSeries(XYChart.Series series) { + if (activeSeries.contains(series)) { + return; + } + + chart.getData().add(series); + activeSeries.add(series); + legendToggleBySeriesName.get(getSeriesId(series)).setSelected(true); + updateChartAfterDataChange(); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Data + /////////////////////////////////////////////////////////////////////////////////////////// + + protected abstract void applyData(); + + /** + * Implementations define which series will be used for setBoundsForTimelineNavigation + */ + protected abstract void initBoundsForTimelineNavigation(); + + /** + * @param data The series data which determines the min/max x values for the time line navigation. + * If not applicable initBoundsForTimelineNavigation requires custom implementation. + */ + protected void setBoundsForTimelineNavigation(ObservableList> data) { + model.initBounds(data); + xAxis.setLowerBound(model.getLowerBound().doubleValue()); + xAxis.setUpperBound(model.getUpperBound().doubleValue()); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Handlers triggering a data/chart update + /////////////////////////////////////////////////////////////////////////////////////////// + + private void onTimeIntervalChanged(Toggle newValue) { + TemporalAdjusterModel.Interval interval = (TemporalAdjusterModel.Interval) newValue.getUserData(); + applyTemporalAdjuster(interval.getAdjuster()); + model.invalidateCache(); + applyData(); + updateChartAfterDataChange(); + } + + private void onTimelineChanged() { + double leftPos = timelineNavigation.getDividerPositions()[0]; + double rightPos = timelineNavigation.getDividerPositions()[1]; + model.onTimelineNavigationChanged(leftPos, rightPos); + // We need to update as model might have adjusted the values + timelineNavigation.setDividerPositions(model.getDividerPositions()[0], model.getDividerPositions()[1]); + + model.invalidateCache(); + applyData(); + updateChartAfterDataChange(); + } + + private void onSelectLegendToggle(XYChart.Series series) { + boolean isSelected = legendToggleBySeriesName.get(getSeriesId(series)).isSelected(); + // If we have set that flag we deselect all other toggles + if (isRadioButtonBehaviour) { + new ArrayList<>(chart.getData()).stream() // We need to copy to a new list to avoid ConcurrentModificationException + .filter(activeSeries::contains) + .forEach(seriesToRemove -> { + chart.getData().remove(seriesToRemove); + String seriesId = getSeriesId(seriesToRemove); + activeSeries.remove(seriesToRemove); + legendToggleBySeriesName.get(seriesId).setSelected(false); + }); + } + + if (isSelected) { + chart.getData().add(series); + activeSeries.add(series); + //model.invalidateCache(); + applyData(); + + if (isRadioButtonBehaviour) { + // We support different y-axis formats only if isRadioButtonBehaviour is set, otherwise we would get + // mixed data on y-axis + onSetYAxisFormatter(series); + } + } else if (!isRadioButtonBehaviour) { // if isRadioButtonBehaviour we have removed it already via the code above + chart.getData().remove(series); + activeSeries.remove(series); + + } + updateChartAfterDataChange(); + } + + /////////////////////////////////////////////////////////////////////////////////////////// + // Chart update after data change + /////////////////////////////////////////////////////////////////////////////////////////// + + // Update of the chart data can be triggered by: + // 1. activate() + // 2. TimeInterval toggle change + // 3. Timeline navigation change + // 4. Legend/series toggle change + + // Timeline navigation and legend/series toggles get reset at activate. + // Time interval toggle keeps its state at screen changes. + protected void updateChartAfterDataChange() { + // If a series got no data points after update we need to clear it from the chart + cleanupDanglingSeries(); + + // Hides symbols if too many data points are created + updateSymbolsVisibility(); + + // When series gets added/removed the JavaFx charts framework would try to apply styles by the index of + // addition, but we want to use a static color assignment which is synced with the legend color. + applySeriesStyles(); + + // Set tooltip on symbols + applyTooltip(); + } + + private void cleanupDanglingSeries() { + List> activeSeriesList = new ArrayList<>(activeSeries); + activeSeriesList.forEach(series -> { + ObservableList> seriesOnChart = chart.getData(); + if (series.getData().isEmpty()) { + seriesOnChart.remove(series); + } else if (!seriesOnChart.contains(series)) { + seriesOnChart.add(series); + } + }); + } + + private void updateSymbolsVisibility() { + maxDataPointsForShowingSymbols = 100; + long numDataPoints = chart.getData().stream() + .map(XYChart.Series::getData) + .mapToLong(List::size) + .max() + .orElse(0); + boolean prevValue = chart.getCreateSymbols(); + boolean newValue = numDataPoints < maxDataPointsForShowingSymbols; + if (prevValue != newValue) { + chart.setCreateSymbols(newValue); + } + } + // The chart framework assigns the colored depending on the order it got added, but want to keep colors // the same so they match with the legend toggle. - protected void applySeriesStyles() { + private void applySeriesStyles() { for (int index = 0; index < chart.getData().size(); index++) { XYChart.Series series = chart.getData().get(index); int staticIndex = seriesIndexMap.get(getSeriesId(series)); @@ -435,6 +654,25 @@ protected void applySeriesStyles() { } } + private void applyTooltip() { + chart.getData().forEach(series -> { + series.getData().forEach(data -> { + Node node = data.getNode(); + if (node == null) { + return; + } + String xValue = model.getTooltipDateConverter(data.getXValue()); + String yValue = model.getYAxisStringConverter().toString(data.getYValue()); + Tooltip.install(node, new Tooltip(Res.get("dao.factsAndFigures.supply.chart.tradeFee.toolTip", yValue, xValue))); + }); + }); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Utils + /////////////////////////////////////////////////////////////////////////////////////////// + private void removeStyles(Node node) { for (int i = 0; i < getMaxSeriesSize(); i++) { node.getStyleClass().remove("default-color" + i); @@ -443,8 +681,10 @@ private void removeStyles(Node node) { private Set getNodesForStyle(Node node, String style) { Set result = new HashSet<>(); - for (int i = 0; i < getMaxSeriesSize(); i++) { - result.addAll(node.lookupAll(String.format(style, i))); + if (node != null) { + for (int i = 0; i < getMaxSeriesSize(); i++) { + result.addAll(node.lookupAll(String.format(style, i))); + } } return result; } @@ -454,8 +694,8 @@ private int getMaxSeriesSize() { return maxSeriesSize; } - private Optional findToggleByTemporalAdjuster(TemporalAdjuster adjuster) { - return timeUnitToggleGroup.getToggles().stream() + private Optional findTimeIntervalToggleByTemporalAdjuster(TemporalAdjuster adjuster) { + return timeIntervalToggleGroup.getToggles().stream() .filter(toggle -> ((TemporalAdjusterModel.Interval) toggle.getUserData()).getAdjuster().equals(adjuster)) .findAny(); } @@ -464,67 +704,4 @@ private Optional findToggleByTemporalAdjuster(TemporalAdjuster adjuster) protected String getSeriesId(XYChart.Series series) { return series.getName(); } - - /////////////////////////////////////////////////////////////////////////////////////////// - // Timeline navigation - /////////////////////////////////////////////////////////////////////////////////////////// - - private void onTimelineChanged() { - double leftPos = splitPane.getDividerPositions()[0]; - double rightPos = splitPane.getDividerPositions()[1]; - // We need to snap into the 0 and 1 values once we are close as otherwise once navigation has been used we - // would not get back to exact 0 or 1. Not clear why but might be rounding issues from values at x positions of - // drag operations. - if (leftPos < 0.01) { - leftPos = 0; - } - if (rightPos > 0.99) { - rightPos = 1; - } - dividerPositions[0] = leftPos; - dividerPositions[1] = rightPos; - splitPane.setDividerPositions(leftPos, rightPos); - Tuple2 fromToTuple = model.timelinePositionToEpochSeconds(leftPos, rightPos); - updateData(model.setAndGetPredicate(fromToTuple)); - model.notifyListeners(fromToTuple); - } - - private void initDividerMouseHandlers() { - // No API access to dividers ;-( only via css lookup hack (https://stackoverflow.com/questions/40707295/how-to-add-listener-to-divider-position?rq=1) - // Need to be done after added to scene and call requestLayout and applyCss. We keep it in a list atm - // and set action handler in activate. - splitPane.requestLayout(); - splitPane.applyCss(); - for (Node node : splitPane.lookupAll(".split-pane-divider")) { - dividerNodes.add(node); - node.setOnMouseReleased(e -> onTimelineChanged()); - } - } - - private void onMouseDragged(MouseEvent e) { - if (pressed) { - double newX = e.getX(); - double width = splitPane.getWidth(); - double relativeDelta = (x - newX) / width; - double leftPos = splitPane.getDividerPositions()[0] - relativeDelta; - double rightPos = splitPane.getDividerPositions()[1] - relativeDelta; - dividerPositions[0] = leftPos; - dividerPositions[1] = rightPos; - splitPane.setDividerPositions(leftPos, rightPos); - x = newX; - } - } - - private void onMouseReleasedCenter(MouseEvent e) { - pressed = false; - onTimelineChanged(); - } - - private void onMousePressedSplitPane(MouseEvent e) { - x = e.getX(); - } - - private void onMousePressedCenter(MouseEvent e) { - pressed = true; - } } diff --git a/desktop/src/main/java/bisq/desktop/components/chart/ChartViewModel.java b/desktop/src/main/java/bisq/desktop/components/chart/ChartViewModel.java index c5638392206..a4cdd60debe 100644 --- a/desktop/src/main/java/bisq/desktop/components/chart/ChartViewModel.java +++ b/desktop/src/main/java/bisq/desktop/components/chart/ChartViewModel.java @@ -17,96 +17,227 @@ package bisq.desktop.components.chart; -import bisq.desktop.common.model.ActivatableViewModel; +import bisq.desktop.common.model.ActivatableWithDataModel; +import bisq.desktop.util.DisplayUtils; import bisq.common.util.Tuple2; +import javafx.scene.chart.XYChart; + +import javafx.util.StringConverter; + import java.time.temporal.TemporalAdjuster; -import java.util.HashSet; -import java.util.Set; -import java.util.function.Predicate; +import java.util.Date; +import java.util.List; +import java.util.Map; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; +import lombok.Getter; import lombok.extern.slf4j.Slf4j; @Slf4j -public abstract class ChartViewModel extends ActivatableViewModel { - public interface Listener { - /** - * @param fromDate Epoch date in millis for earliest data - * @param toDate Epoch date in millis for latest data - */ - void onDateFilterChanged(long fromDate, long toDate); +public abstract class ChartViewModel extends ActivatableWithDataModel { + private final static double LEFT_TIMELINE_SNAP_VALUE = 0.01; + private final static double RIGHT_TIMELINE_SNAP_VALUE = 0.99; + + @Getter + private final Double[] dividerPositions = new Double[]{0d, 1d}; + @Getter + protected Number lowerBound; + @Getter + protected Number upperBound; + @Getter + protected String dateFormatPatters = "dd MMM\nyyyy"; + + public ChartViewModel(T dataModel) { + super(dataModel); } - protected Number lowerBound, upperBound; - protected final Set listeners = new HashSet<>(); + @Override + public void activate() { + dividerPositions[0] = 0d; + dividerPositions[1] = 1d; + } - private Predicate predicate = e -> true; - public ChartViewModel() { + /////////////////////////////////////////////////////////////////////////////////////////// + // TimerInterval/TemporalAdjuster + /////////////////////////////////////////////////////////////////////////////////////////// + + protected void applyTemporalAdjuster(TemporalAdjuster temporalAdjuster) { + dataModel.setTemporalAdjuster(temporalAdjuster); + } + + void setDateFormatPattern(TemporalAdjusterModel.Interval interval) { + switch (interval) { + case YEAR: + dateFormatPatters = "yyyy"; + break; + case MONTH: + dateFormatPatters = "MMM\nyyyy"; + break; + default: + dateFormatPatters = "MMM dd\nyyyy"; + break; + } + } + + protected TemporalAdjuster getTemporalAdjuster() { + return dataModel.getTemporalAdjuster(); } /////////////////////////////////////////////////////////////////////////////////////////// - // Lifecycle + // TimelineNavigation /////////////////////////////////////////////////////////////////////////////////////////// - @Override - public void activate() { + void onTimelineNavigationChanged(double leftPos, double rightPos) { + long from, to; + + // We need to snap into the 0 and 1 values once we are close as otherwise once navigation has been used we + // would not get back to exact 0 or 1. Not clear why but might be rounding issues from values at x positions of + // drag operations. + if (leftPos < LEFT_TIMELINE_SNAP_VALUE) { + leftPos = 0; + } + if (rightPos > RIGHT_TIMELINE_SNAP_VALUE) { + rightPos = 1; + } + dividerPositions[0] = leftPos; + dividerPositions[1] = rightPos; + + long lowerBoundAsLong = lowerBound.longValue(); + long totalRange = upperBound.longValue() - lowerBoundAsLong; + + // TODO find better solution + // The TemporalAdjusters map dates to the lower bound (e.g. 1.1.2016) but our from date is the date of + // the first data entry so if we filter by that we would exclude the first year data in case YEAR was selected + // A trade with data 3.May.2016 gets mapped to 1.1.2016 and our from date will be April 2016, so we would + // filter that. It is a bit tricky to sync the TemporalAdjusters with our date filter. To include at least in + // the case when we have not set the date filter (left =0 / right =1) we set from date to epoch time 0 and + // to date to one year ahead to be sure we include all. + + if (leftPos == 0) { + from = 0; + } else { + from = (long) (lowerBoundAsLong + totalRange * leftPos); + } + if (rightPos == 1) { + to = new Date().getTime() / 1000 + TimeUnit.DAYS.toSeconds(365); + } else { + to = (long) (lowerBoundAsLong + totalRange * rightPos); + } + + dataModel.setDateFilter(from, to); } - @Override - public void deactivate() { + void onTimelineMouseDrag(double leftPos, double rightPos) { + // Limit drag operation if we have hit a boundary + if (leftPos > LEFT_TIMELINE_SNAP_VALUE) { + dividerPositions[1] = rightPos; + } + if (rightPos < RIGHT_TIMELINE_SNAP_VALUE) { + dividerPositions[0] = leftPos; + } + } + + void initBounds(List> data1, + List> data2) { + Tuple2 xMinMaxTradeFee = getMinMax(data1); + Tuple2 xMinMaxCompensationRequest = getMinMax(data2); + + lowerBound = Math.min(xMinMaxTradeFee.first, xMinMaxCompensationRequest.first); + upperBound = Math.max(xMinMaxTradeFee.second, xMinMaxCompensationRequest.second); + } + + void initBounds(List> data) { + Tuple2 xMinMaxTradeFee = getMinMax(data); + lowerBound = xMinMaxTradeFee.first; + upperBound = xMinMaxTradeFee.second; } /////////////////////////////////////////////////////////////////////////////////////////// - // API + // Chart /////////////////////////////////////////////////////////////////////////////////////////// - public void addListener(Listener listener) { - listeners.add(listener); + StringConverter getTimeAxisStringConverter() { + return new StringConverter<>() { + @Override + public String toString(Number epochSeconds) { + Date date = new Date(epochSeconds.longValue() * 1000); + return DisplayUtils.formatDateAxis(date, getDateFormatPatters()); + } + + @Override + public Number fromString(String string) { + return 0; + } + }; } - public void removeListener(Listener listener) { - listeners.remove(listener); + protected StringConverter getYAxisStringConverter() { + return new StringConverter<>() { + @Override + public String toString(Number value) { + return String.valueOf(value); + } + + @Override + public Number fromString(String string) { + return null; + } + }; } - public Number getLowerBound() { - return lowerBound; + String getTooltipDateConverter(Number date) { + return getTimeAxisStringConverter().toString(date).replace("\n", " "); } - public Number getUpperBound() { - return upperBound; + protected String getTooltipValueConverter(Number value) { + return getYAxisStringConverter().toString(value); } - Tuple2 timelinePositionToEpochSeconds(double leftPos, double rightPos) { - long lowerBoundAsLong = lowerBound.longValue(); - long totalRange = upperBound.longValue() - lowerBoundAsLong; - double fromDateSec = lowerBoundAsLong + totalRange * leftPos; - double toDateSec = lowerBoundAsLong + totalRange * rightPos; - return new Tuple2<>(fromDateSec, toDateSec); + + /////////////////////////////////////////////////////////////////////////////////////////// + // Data + /////////////////////////////////////////////////////////////////////////////////////////// + + protected void invalidateCache() { + dataModel.invalidateCache(); } - protected abstract void applyTemporalAdjuster(TemporalAdjuster temporalAdjuster); - protected abstract TemporalAdjuster getTemporalAdjuster(); + /////////////////////////////////////////////////////////////////////////////////////////// + // Utils + /////////////////////////////////////////////////////////////////////////////////////////// - public Predicate getPredicate() { - return predicate; + protected List> toChartData(Map map) { + return map.entrySet().stream() + .map(entry -> new XYChart.Data(entry.getKey(), entry.getValue())) + .collect(Collectors.toList()); } - Predicate setAndGetPredicate(Tuple2 fromToTuple) { - predicate = value -> value >= fromToTuple.first && value <= fromToTuple.second; - return predicate; + protected List> toChartDoubleData(Map map) { + return map.entrySet().stream() + .map(entry -> new XYChart.Data(entry.getKey(), entry.getValue())) + .collect(Collectors.toList()); } - void notifyListeners(Tuple2 fromToTuple) { - // We use millis for our listeners - long first = fromToTuple.first.longValue() * 1000; - long second = fromToTuple.second.longValue() * 1000; - listeners.forEach(l -> l.onDateFilterChanged(first, second)); + protected List> toChartLongData(Map map) { + return map.entrySet().stream() + .map(entry -> new XYChart.Data(entry.getKey(), entry.getValue())) + .collect(Collectors.toList()); } + private Tuple2 getMinMax(List> chartData) { + long min = Long.MAX_VALUE, max = 0; + for (XYChart.Data data : chartData) { + min = Math.min(data.getXValue().longValue(), min); + max = Math.max(data.getXValue().longValue(), max); + } + return new Tuple2<>((double) min, (double) max); + } } diff --git a/desktop/src/main/java/bisq/desktop/components/chart/TemporalAdjusterModel.java b/desktop/src/main/java/bisq/desktop/components/chart/TemporalAdjusterModel.java index fd8634caf67..cd029826fe1 100644 --- a/desktop/src/main/java/bisq/desktop/components/chart/TemporalAdjusterModel.java +++ b/desktop/src/main/java/bisq/desktop/components/chart/TemporalAdjusterModel.java @@ -17,13 +17,12 @@ package bisq.desktop.components.chart; +import java.time.DayOfWeek; import java.time.Instant; import java.time.ZoneId; import java.time.temporal.TemporalAdjuster; import java.time.temporal.TemporalAdjusters; -import java.util.function.Predicate; - import lombok.Getter; import lombok.extern.slf4j.Slf4j; @@ -34,7 +33,7 @@ public class TemporalAdjusterModel { public enum Interval { YEAR(TemporalAdjusters.firstDayOfYear()), MONTH(TemporalAdjusters.firstDayOfMonth()), - WEEK(TemporalAdjusters.ofDateAdjuster(date -> date.plusWeeks(1))), + WEEK(TemporalAdjusters.next(DayOfWeek.MONDAY)), DAY(TemporalAdjusters.ofDateAdjuster(d -> d)); @Getter @@ -68,8 +67,4 @@ public long toTimeInterval(Instant instant, TemporalAdjuster temporalAdjuster) { .toInstant() .getEpochSecond(); } - - public Predicate getPredicate(long fromDate, long toDate) { - return value -> value >= fromDate / 1000 && value <= toDate / 1000; - } } diff --git a/desktop/src/main/java/bisq/desktop/main/dao/economy/dashboard/BsqDashboardView.java b/desktop/src/main/java/bisq/desktop/main/dao/economy/dashboard/BsqDashboardView.java index 693ec87cd61..152cf23bbb3 100644 --- a/desktop/src/main/java/bisq/desktop/main/dao/economy/dashboard/BsqDashboardView.java +++ b/desktop/src/main/java/bisq/desktop/main/dao/economy/dashboard/BsqDashboardView.java @@ -20,6 +20,10 @@ import bisq.desktop.common.view.ActivatableView; import bisq.desktop.common.view.FxmlView; import bisq.desktop.components.TextFieldWithIcon; +import bisq.desktop.components.TitledGroupBg; +import bisq.desktop.main.dao.economy.dashboard.price.PriceChartView; +import bisq.desktop.main.dao.economy.dashboard.volume.VolumeChartView; +import bisq.desktop.util.Layout; import bisq.core.dao.DaoFacade; import bisq.core.dao.state.DaoStateListener; @@ -28,7 +32,6 @@ import bisq.core.locale.Res; import bisq.core.monetary.Price; import bisq.core.provider.price.PriceFeedService; -import bisq.core.trade.statistics.TradeStatistics3; import bisq.core.trade.statistics.TradeStatisticsManager; import bisq.core.user.Preferences; import bisq.core.util.AveragePriceUtil; @@ -44,9 +47,6 @@ import de.jensd.fx.fontawesome.AwesomeIcon; -import javafx.scene.chart.AreaChart; -import javafx.scene.chart.NumberAxis; -import javafx.scene.chart.XYChart; import javafx.scene.control.Label; import javafx.scene.control.TextField; import javafx.scene.layout.AnchorPane; @@ -54,32 +54,15 @@ import javafx.scene.layout.VBox; import javafx.geometry.Insets; -import javafx.geometry.Side; import javafx.beans.value.ChangeListener; import javafx.collections.ObservableList; -import javafx.util.StringConverter; - -import java.time.LocalDate; -import java.time.LocalDateTime; -import java.time.OffsetDateTime; -import java.time.ZoneId; -import java.time.ZonedDateTime; -import java.time.format.DateTimeFormatter; -import java.time.format.FormatStyle; -import java.time.temporal.TemporalAdjuster; -import java.time.temporal.TemporalAdjusters; - -import java.util.Comparator; -import java.util.HashMap; -import java.util.List; -import java.util.Map; import java.util.Optional; -import java.util.stream.Collectors; import static bisq.desktop.util.FormBuilder.addLabelWithSubText; +import static bisq.desktop.util.FormBuilder.addTitledGroupBg; import static bisq.desktop.util.FormBuilder.addTopLabelReadOnlyTextField; import static bisq.desktop.util.FormBuilder.addTopLabelTextFieldWithIcon; @@ -87,26 +70,21 @@ @FxmlView public class BsqDashboardView extends ActivatableView implements DaoStateListener { - private static final String DAY = "day"; - private static final Map ADJUSTERS = new HashMap<>(); - + private final PriceChartView priceChartView; + private final VolumeChartView volumeChartView; private final DaoFacade daoFacade; private final TradeStatisticsManager tradeStatisticsManager; private final PriceFeedService priceFeedService; private final Preferences preferences; private final BsqFormatter bsqFormatter; - private ChangeListener priceChangeListener; - - private AreaChart bsqPriceChart; - private XYChart.Series seriesBSQPrice; - private TextField avgPrice90TextField, avgUSDPrice90TextField, marketCapTextField, availableAmountTextField; private TextFieldWithIcon avgPrice30TextField, avgUSDPrice30TextField; private Label marketPriceLabel; - private Coin availableAmount; + private ChangeListener priceChangeListener; private int gridRow = 0; + private Coin availableAmount; private Price avg30DayUSDPrice; @@ -115,11 +93,15 @@ public class BsqDashboardView extends ActivatableView implements /////////////////////////////////////////////////////////////////////////////////////////// @Inject - public BsqDashboardView(DaoFacade daoFacade, + public BsqDashboardView(PriceChartView priceChartView, + VolumeChartView volumeChartView, + DaoFacade daoFacade, TradeStatisticsManager tradeStatisticsManager, PriceFeedService priceFeedService, Preferences preferences, BsqFormatter bsqFormatter) { + this.priceChartView = priceChartView; + this.volumeChartView = volumeChartView; this.daoFacade = daoFacade; this.tradeStatisticsManager = tradeStatisticsManager; this.priceFeedService = priceFeedService; @@ -129,10 +111,9 @@ public BsqDashboardView(DaoFacade daoFacade, @Override public void initialize() { - ADJUSTERS.put(DAY, TemporalAdjusters.ofDateAdjuster(d -> d)); - - createKPIs(); - createChart(); + createTextFields(); + createPriceChart(); + createTradeChart(); priceChangeListener = (observable, oldValue, newValue) -> { updatePrice(); @@ -142,35 +123,6 @@ public void initialize() { }; } - private void createKPIs() { - - Tuple3 marketPriceBox = addLabelWithSubText(root, gridRow++, "", ""); - marketPriceLabel = marketPriceBox.first; - marketPriceLabel.getStyleClass().add("dao-kpi-big"); - - marketPriceBox.second.getStyleClass().add("dao-kpi-subtext"); - - avgUSDPrice90TextField = addTopLabelReadOnlyTextField(root, ++gridRow, - Res.get("dao.factsAndFigures.dashboard.avgUSDPrice90")).second; - - avgUSDPrice30TextField = addTopLabelTextFieldWithIcon(root, gridRow, 1, - Res.get("dao.factsAndFigures.dashboard.avgUSDPrice30"), -15).second; - AnchorPane.setRightAnchor(avgUSDPrice30TextField.getIconLabel(), 10d); - - avgPrice90TextField = addTopLabelReadOnlyTextField(root, ++gridRow, - Res.get("dao.factsAndFigures.dashboard.avgPrice90")).second; - - avgPrice30TextField = addTopLabelTextFieldWithIcon(root, gridRow, 1, - Res.get("dao.factsAndFigures.dashboard.avgPrice30"), -15).second; - AnchorPane.setRightAnchor(avgPrice30TextField.getIconLabel(), 10d); - - marketCapTextField = addTopLabelReadOnlyTextField(root, ++gridRow, - Res.get("dao.factsAndFigures.dashboard.marketCap")).second; - - availableAmountTextField = addTopLabelReadOnlyTextField(root, gridRow, 1, - Res.get("dao.factsAndFigures.dashboard.availableAmount")).second; - } - @Override protected void activate() { daoFacade.addBsqStateListener(this); @@ -178,7 +130,6 @@ protected void activate() { updateWithBsqBlockChainData(); updatePrice(); - updateChartData(); updateAveragePriceFields(avgPrice90TextField, avgPrice30TextField, false); updateAveragePriceFields(avgUSDPrice90TextField, avgUSDPrice30TextField, true); updateMarketCap(); @@ -191,6 +142,7 @@ protected void deactivate() { priceFeedService.updateCounterProperty().removeListener(priceChangeListener); } + /////////////////////////////////////////////////////////////////////////////////////////// // DaoStateListener /////////////////////////////////////////////////////////////////////////////////////////// @@ -202,106 +154,78 @@ public void onParseBlockCompleteAfterBatchProcessing(Block block) { /////////////////////////////////////////////////////////////////////////////////////////// - // Private + // Build UI /////////////////////////////////////////////////////////////////////////////////////////// - private void createChart() { - NumberAxis xAxis = new NumberAxis(); - xAxis.setForceZeroInRange(false); - xAxis.setAutoRanging(true); - xAxis.setTickLabelGap(6); - xAxis.setTickMarkVisible(false); - xAxis.setMinorTickVisible(false); - - xAxis.setTickLabelFormatter(new StringConverter<>() { - @Override - public String toString(Number timestamp) { - LocalDateTime localDateTime = LocalDateTime.ofEpochSecond(timestamp.longValue(), - 0, OffsetDateTime.now(ZoneId.systemDefault()).getOffset()); - return localDateTime.format(DateTimeFormatter.ofLocalizedDate(FormatStyle.MEDIUM)); - } + private void createTextFields() { + Tuple3 marketPriceBox = addLabelWithSubText(root, gridRow++, "", ""); + marketPriceLabel = marketPriceBox.first; + marketPriceLabel.getStyleClass().add("dao-kpi-big"); - @Override - public Number fromString(String string) { - return 0; - } - }); - - NumberAxis yAxis = new NumberAxis(); - yAxis.setForceZeroInRange(false); - yAxis.setSide(Side.RIGHT); - yAxis.setAutoRanging(true); - yAxis.setTickMarkVisible(false); - yAxis.setMinorTickVisible(false); - yAxis.setTickLabelGap(5); - yAxis.setTickLabelFormatter(new StringConverter<>() { - @Override - public String toString(Number marketPrice) { - return bsqFormatter.formatBTCWithCode(marketPrice.longValue()); - } + marketPriceBox.second.getStyleClass().add("dao-kpi-subtext"); - @Override - public Number fromString(String string) { - return 0; - } - }); + avgUSDPrice90TextField = addTopLabelReadOnlyTextField(root, ++gridRow, + Res.get("dao.factsAndFigures.dashboard.avgUSDPrice90")).second; - seriesBSQPrice = new XYChart.Series<>(); - seriesBSQPrice.setName("Price in BTC for 1 BSQ"); + avgUSDPrice30TextField = addTopLabelTextFieldWithIcon(root, gridRow, 1, + Res.get("dao.factsAndFigures.dashboard.avgUSDPrice30"), -15).second; + AnchorPane.setRightAnchor(avgUSDPrice30TextField.getIconLabel(), 10d); - bsqPriceChart = new AreaChart<>(xAxis, yAxis); - bsqPriceChart.setLegendVisible(false); - bsqPriceChart.setAnimated(false); - bsqPriceChart.setId("charts-dao"); - bsqPriceChart.setMinHeight(320); - bsqPriceChart.setPrefHeight(bsqPriceChart.getMinHeight()); - bsqPriceChart.setCreateSymbols(true); - bsqPriceChart.setPadding(new Insets(0)); - bsqPriceChart.getData().add(seriesBSQPrice); + avgPrice90TextField = addTopLabelReadOnlyTextField(root, ++gridRow, + Res.get("dao.factsAndFigures.dashboard.avgPrice90")).second; - AnchorPane chartPane = new AnchorPane(); - chartPane.getStyleClass().add("chart-pane"); + avgPrice30TextField = addTopLabelTextFieldWithIcon(root, gridRow, 1, + Res.get("dao.factsAndFigures.dashboard.avgPrice30"), -15).second; + AnchorPane.setRightAnchor(avgPrice30TextField.getIconLabel(), 10d); - AnchorPane.setTopAnchor(bsqPriceChart, 15d); - AnchorPane.setBottomAnchor(bsqPriceChart, 10d); - AnchorPane.setLeftAnchor(bsqPriceChart, 25d); - AnchorPane.setRightAnchor(bsqPriceChart, 10d); + marketCapTextField = addTopLabelReadOnlyTextField(root, ++gridRow, + Res.get("dao.factsAndFigures.dashboard.marketCap")).second; + + availableAmountTextField = addTopLabelReadOnlyTextField(root, gridRow, 1, + Res.get("dao.factsAndFigures.dashboard.availableAmount")).second; + } - chartPane.getChildren().add(bsqPriceChart); + private void createPriceChart() { + addTitledGroupBg(root, ++gridRow, 2, + Res.get("dao.factsAndFigures.supply.priceChat"), Layout.FLOATING_LABEL_DISTANCE); - GridPane.setRowIndex(chartPane, ++gridRow); + priceChartView.initialize(); + VBox chartContainer = priceChartView.getRoot(); + + AnchorPane chartPane = new AnchorPane(); + chartPane.getStyleClass().add("chart-pane"); + AnchorPane.setTopAnchor(chartContainer, 15d); + AnchorPane.setBottomAnchor(chartContainer, 10d); + AnchorPane.setLeftAnchor(chartContainer, 25d); + AnchorPane.setRightAnchor(chartContainer, 10d); GridPane.setColumnSpan(chartPane, 2); - GridPane.setMargin(chartPane, new Insets(10, 0, 0, 0)); + GridPane.setRowIndex(chartPane, ++gridRow); + GridPane.setMargin(chartPane, new Insets(Layout.COMPACT_FIRST_ROW_AND_COMPACT_GROUP_DISTANCE, 0, 0, 0)); + chartPane.getChildren().add(chartContainer); - root.getChildren().addAll(chartPane); + root.getChildren().add(chartPane); } - private void updateChartData() { - updateBsqPriceData(); - } + private void createTradeChart() { + TitledGroupBg titledGroupBg = addTitledGroupBg(root, ++gridRow, 2, + Res.get("dao.factsAndFigures.supply.volumeChat"), Layout.FLOATING_LABEL_DISTANCE); + titledGroupBg.getStyleClass().add("last"); // hides separator as we add a second TitledGroupBg + + volumeChartView.initialize(); + VBox chartContainer = volumeChartView.getRoot(); - private void updateBsqPriceData() { - seriesBSQPrice.getData().clear(); - - Map> bsqPriceByDate = tradeStatisticsManager.getObservableTradeStatisticsSet().stream() - .filter(e -> e.getCurrency().equals("BSQ")) - .sorted(Comparator.comparing(TradeStatistics3::getDateAsLong)) - .collect(Collectors.groupingBy(item -> new java.sql.Date(item.getDateAsLong()).toLocalDate() - .with(ADJUSTERS.get(DAY)))); - - List> updatedBSQPrice = bsqPriceByDate.keySet().stream() - .map(e -> { - ZonedDateTime zonedDateTime = e.atStartOfDay(ZoneId.systemDefault()); - return new XYChart.Data(zonedDateTime.toInstant().getEpochSecond(), bsqPriceByDate.get(e).stream() - .map(TradeStatistics3::getTradePrice) - .mapToDouble(Price::getValue) - .average() - .orElse(Double.NaN) - ); - }) - .collect(Collectors.toList()); - - seriesBSQPrice.getData().setAll(updatedBSQPrice); + AnchorPane chartPane = new AnchorPane(); + chartPane.getStyleClass().add("chart-pane"); + AnchorPane.setTopAnchor(chartContainer, 15d); + AnchorPane.setBottomAnchor(chartContainer, 10d); + AnchorPane.setLeftAnchor(chartContainer, 25d); + AnchorPane.setRightAnchor(chartContainer, 10d); + GridPane.setColumnSpan(chartPane, 2); + GridPane.setRowIndex(chartPane, ++gridRow); + GridPane.setMargin(chartPane, new Insets(Layout.COMPACT_FIRST_ROW_AND_COMPACT_GROUP_DISTANCE, 0, 0, 0)); + chartPane.getChildren().add(chartContainer); + + root.getChildren().add(chartPane); } private void updateWithBsqBlockChainData() { @@ -309,7 +233,6 @@ private void updateWithBsqBlockChainData() { Coin issuedAmountFromCompRequests = Coin.valueOf(daoFacade.getTotalIssuedAmount(IssuanceType.COMPENSATION)); Coin issuedAmountFromReimbursementRequests = Coin.valueOf(daoFacade.getTotalIssuedAmount(IssuanceType.REIMBURSEMENT)); Coin totalConfiscatedAmount = Coin.valueOf(daoFacade.getTotalAmountOfConfiscatedTxOutputs()); - // Contains burnt fee and invalidated bsq due invalid txs Coin totalAmountOfBurntBsq = Coin.valueOf(daoFacade.getTotalAmountOfBurntBsq()); availableAmount = issuedAmountFromGenesis @@ -326,8 +249,6 @@ private void updatePrice() { if (optionalBsqPrice.isPresent()) { Price bsqPrice = optionalBsqPrice.get(); marketPriceLabel.setText(FormattingUtils.formatPrice(bsqPrice) + " BSQ/BTC"); - - updateChartData(); } else { marketPriceLabel.setText(Res.get("shared.na")); } @@ -370,7 +291,7 @@ private long updateAveragePriceField(TextField textField, int days, boolean isUS Price bsqPrice = tuple.second; if (isUSDField) { - textField.setText(usdPrice + " USD/BSQ"); + textField.setText(usdPrice + " BSQ/USD"); if (days == 30) { avg30DayUSDPrice = usdPrice; } diff --git a/desktop/src/main/java/bisq/desktop/main/dao/economy/dashboard/price/PriceChartDataModel.java b/desktop/src/main/java/bisq/desktop/main/dao/economy/dashboard/price/PriceChartDataModel.java new file mode 100644 index 00000000000..e10859e0bfb --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/main/dao/economy/dashboard/price/PriceChartDataModel.java @@ -0,0 +1,194 @@ +/* + * This file is part of Bisq. + * + * Bisq is free software: you can redistribute it and/or modify it + * under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at + * your option) any later version. + * + * Bisq is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public + * License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Bisq. If not, see . + */ + +package bisq.desktop.main.dao.economy.dashboard.price; + +import bisq.desktop.components.chart.ChartDataModel; + +import bisq.core.trade.statistics.TradeStatistics3; +import bisq.core.trade.statistics.TradeStatisticsManager; + +import bisq.common.util.MathUtils; + +import javax.inject.Inject; + +import java.time.Instant; + +import java.util.AbstractMap; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.function.Function; +import java.util.function.Predicate; +import java.util.stream.Collectors; + +import lombok.extern.slf4j.Slf4j; + +@Slf4j +public class PriceChartDataModel extends ChartDataModel { + private final TradeStatisticsManager tradeStatisticsManager; + private Map bsqUsdPriceByInterval, bsqBtcPriceByInterval, btcUsdPriceByInterval; + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Constructor + /////////////////////////////////////////////////////////////////////////////////////////// + + @Inject + public PriceChartDataModel(TradeStatisticsManager tradeStatisticsManager) { + super(); + + this.tradeStatisticsManager = tradeStatisticsManager; + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Data + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + protected void invalidateCache() { + bsqUsdPriceByInterval = null; + bsqBtcPriceByInterval = null; + btcUsdPriceByInterval = null; + } + + Map getBsqUsdPriceByInterval() { + if (bsqUsdPriceByInterval != null) { + return bsqUsdPriceByInterval; + } + bsqUsdPriceByInterval = getPriceByInterval(tradeStatistics -> tradeStatistics.getCurrency().equals("BSQ") || + tradeStatistics.getCurrency().equals("USD"), + PriceChartDataModel::getAverageBsqUsdPrice); + return bsqUsdPriceByInterval; + } + + Map getBsqBtcPriceByInterval() { + if (bsqBtcPriceByInterval != null) { + return bsqBtcPriceByInterval; + } + + bsqBtcPriceByInterval = getPriceByInterval(tradeStatistics -> tradeStatistics.getCurrency().equals("BSQ"), + PriceChartDataModel::getAverageBsqBtcPrice); + return bsqBtcPriceByInterval; + } + + Map getBtcUsdPriceByInterval() { + if (btcUsdPriceByInterval != null) { + return btcUsdPriceByInterval; + } + + btcUsdPriceByInterval = getPriceByInterval(tradeStatistics -> tradeStatistics.getCurrency().equals("USD"), + PriceChartDataModel::getAverageBtcUsdPrice); + return btcUsdPriceByInterval; + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Average price functions + /////////////////////////////////////////////////////////////////////////////////////////// + + private static double getAverageBsqUsdPrice(List list) { + double sumBsq = 0; + double sumBtcFromBsqTrades = 0; + double sumBtcFromUsdTrades = 0; + double sumUsd = 0; + for (TradeStatistics3 tradeStatistics : list) { + if (tradeStatistics.getCurrency().equals("BSQ")) { + sumBsq += getBsqAmount(tradeStatistics); + sumBtcFromBsqTrades += getBtcAmount(tradeStatistics); + } else if (tradeStatistics.getCurrency().equals("USD")) { + sumUsd += getUsdAmount(tradeStatistics); + sumBtcFromUsdTrades += getBtcAmount(tradeStatistics); + } + } + if (sumBsq == 0 || sumBtcFromBsqTrades == 0 || sumBtcFromUsdTrades == 0 || sumUsd == 0) { + return 0d; + } + double averageUsdPrice = sumUsd / sumBtcFromUsdTrades; + return sumBtcFromBsqTrades * averageUsdPrice / sumBsq; + } + + private static double getAverageBsqBtcPrice(List list) { + double sumBsq = 0; + double sumBtc = 0; + for (TradeStatistics3 tradeStatistics : list) { + sumBsq += getBsqAmount(tradeStatistics); + sumBtc += getBtcAmount(tradeStatistics); + } + if (sumBsq == 0 || sumBtc == 0) { + return 0d; + } + return MathUtils.scaleUpByPowerOf10(sumBtc / sumBsq, 8); + } + + private static double getAverageBtcUsdPrice(List list) { + double sumUsd = 0; + double sumBtc = 0; + for (TradeStatistics3 tradeStatistics : list) { + sumUsd += getUsdAmount(tradeStatistics); + sumBtc += getBtcAmount(tradeStatistics); + } + if (sumUsd == 0 || sumBtc == 0) { + return 0d; + } + return sumUsd / sumBtc; + } + + private static long getBtcAmount(TradeStatistics3 tradeStatistics) { + return tradeStatistics.getAmount(); + } + + private static double getUsdAmount(TradeStatistics3 tradeStatistics) { + return MathUtils.scaleUpByPowerOf10(tradeStatistics.getTradeVolume().getValue(), 4); + } + + private static long getBsqAmount(TradeStatistics3 tradeStatistics) { + return tradeStatistics.getTradeVolume().getValue(); + } + + /////////////////////////////////////////////////////////////////////////////////////////// + // Aggregated collection data by interval + /////////////////////////////////////////////////////////////////////////////////////////// + + private Map getPriceByInterval(Predicate collectionFilter, + Function, Double> getAveragePriceFunction) { + return getPriceByInterval(tradeStatisticsManager.getObservableTradeStatisticsSet(), + collectionFilter, + tradeStatistics -> toTimeInterval(Instant.ofEpochMilli(tradeStatistics.getDateAsLong())), + dateFilter, + getAveragePriceFunction); + } + + private Map getPriceByInterval(Collection collection, + Predicate collectionFilter, + Function groupByDateFunction, + Predicate dateFilter, + Function, Double> getAveragePriceFunction) { + return collection.stream() + .filter(collectionFilter) + .collect(Collectors.groupingBy(groupByDateFunction)) + .entrySet() + .stream() + .filter(entry -> dateFilter.test(entry.getKey())) + .map(entry -> new AbstractMap.SimpleEntry<>( + entry.getKey(), + getAveragePriceFunction.apply(entry.getValue()))) + .filter(e -> e.getValue() > 0d) + .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); + } +} diff --git a/desktop/src/main/java/bisq/desktop/main/dao/economy/dashboard/price/PriceChartView.java b/desktop/src/main/java/bisq/desktop/main/dao/economy/dashboard/price/PriceChartView.java new file mode 100644 index 00000000000..d9d69bdc989 --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/main/dao/economy/dashboard/price/PriceChartView.java @@ -0,0 +1,137 @@ +/* + * This file is part of Bisq. + * + * Bisq is free software: you can redistribute it and/or modify it + * under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at + * your option) any later version. + * + * Bisq is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public + * License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Bisq. If not, see . + */ + +package bisq.desktop.main.dao.economy.dashboard.price; + +import bisq.desktop.components.chart.ChartView; + +import bisq.core.locale.Res; + +import javax.inject.Inject; + +import javafx.scene.chart.XYChart; + +import java.util.Collection; +import java.util.List; + +import lombok.extern.slf4j.Slf4j; + +@Slf4j +public class PriceChartView extends ChartView { + private XYChart.Series seriesBsqUsdPrice, seriesBsqBtcPrice, seriesBtcUsdPrice; + + + @Inject + public PriceChartView(PriceChartViewModel model) { + super(model); + + setRadioButtonBehaviour(true); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Chart + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + protected void onSetYAxisFormatter(XYChart.Series series) { + if (series == seriesBsqUsdPrice) { + model.setBsqUsdPriceFormatter(); + } else if (series == seriesBsqBtcPrice) { + model.setBsqBtcPriceFormatter(); + } else { + model.setBtcUsdPriceFormatter(); + } + } + + /////////////////////////////////////////////////////////////////////////////////////////// + // Legend + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + protected Collection> getSeriesForLegend1() { + return List.of(seriesBsqUsdPrice, seriesBsqBtcPrice, seriesBtcUsdPrice); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Timeline navigation + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + protected void initBoundsForTimelineNavigation() { + setBoundsForTimelineNavigation(seriesBsqUsdPrice.getData()); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Series + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + protected void createSeries() { + seriesBsqUsdPrice = new XYChart.Series<>(); + seriesBsqUsdPrice.setName(Res.get("dao.factsAndFigures.supply.bsqUsdPrice")); + seriesIndexMap.put(getSeriesId(seriesBsqUsdPrice), 0); + + seriesBsqBtcPrice = new XYChart.Series<>(); + seriesBsqBtcPrice.setName(Res.get("dao.factsAndFigures.supply.bsqBtcPrice")); + seriesIndexMap.put(getSeriesId(seriesBsqBtcPrice), 1); + + seriesBtcUsdPrice = new XYChart.Series<>(); + seriesBtcUsdPrice.setName(Res.get("dao.factsAndFigures.supply.btcUsdPrice")); + seriesIndexMap.put(getSeriesId(seriesBtcUsdPrice), 2); + } + + @Override + protected void defineAndAddActiveSeries() { + activateSeries(seriesBsqUsdPrice); + onSetYAxisFormatter(seriesBsqUsdPrice); + } + + @Override + protected void activateSeries(XYChart.Series series) { + super.activateSeries(series); + + String seriesId = getSeriesId(series); + if (seriesId.equals(getSeriesId(seriesBsqUsdPrice))) { + seriesBsqUsdPrice.getData().setAll(model.getBsqUsdPriceChartData()); + } else if (seriesId.equals(getSeriesId(seriesBsqBtcPrice))) { + seriesBsqBtcPrice.getData().setAll(model.getBsqBtcPriceChartData()); + } else if (seriesId.equals(getSeriesId(seriesBtcUsdPrice))) { + seriesBtcUsdPrice.getData().setAll(model.getBtcUsdPriceChartData()); + } + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Data + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + protected void applyData() { + if (activeSeries.contains(seriesBsqUsdPrice)) { + seriesBsqUsdPrice.getData().setAll(model.getBsqUsdPriceChartData()); + } + if (activeSeries.contains(seriesBsqBtcPrice)) { + seriesBsqBtcPrice.getData().setAll(model.getBsqBtcPriceChartData()); + } + if (activeSeries.contains(seriesBtcUsdPrice)) { + seriesBtcUsdPrice.getData().setAll(model.getBtcUsdPriceChartData()); + } + } +} diff --git a/desktop/src/main/java/bisq/desktop/main/dao/economy/dashboard/price/PriceChartViewModel.java b/desktop/src/main/java/bisq/desktop/main/dao/economy/dashboard/price/PriceChartViewModel.java new file mode 100644 index 00000000000..891be72d337 --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/main/dao/economy/dashboard/price/PriceChartViewModel.java @@ -0,0 +1,105 @@ +/* + * This file is part of Bisq. + * + * Bisq is free software: you can redistribute it and/or modify it + * under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at + * your option) any later version. + * + * Bisq is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public + * License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Bisq. If not, see . + */ + +package bisq.desktop.main.dao.economy.dashboard.price; + +import bisq.desktop.components.chart.ChartViewModel; + +import bisq.core.locale.GlobalSettings; + +import bisq.common.util.MathUtils; + +import javax.inject.Inject; + +import javafx.scene.chart.XYChart; + +import javafx.util.StringConverter; + +import java.text.DecimalFormat; + +import java.util.List; +import java.util.function.Function; + +import lombok.extern.slf4j.Slf4j; + +@Slf4j +public class PriceChartViewModel extends ChartViewModel { + private Function yAxisFormatter = value -> value + " BSQ/USD"; + private final DecimalFormat priceFormat; + + @Inject + public PriceChartViewModel(PriceChartDataModel dataModel) { + super(dataModel); + + priceFormat = (DecimalFormat) DecimalFormat.getNumberInstance(GlobalSettings.getLocale()); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Chart data + /////////////////////////////////////////////////////////////////////////////////////////// + + List> getBsqUsdPriceChartData() { + return toChartDoubleData(dataModel.getBsqUsdPriceByInterval()); + } + + List> getBsqBtcPriceChartData() { + return toChartDoubleData(dataModel.getBsqBtcPriceByInterval()); + } + + List> getBtcUsdPriceChartData() { + return toChartDoubleData(dataModel.getBtcUsdPriceByInterval()); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Formatters/Converters + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + protected StringConverter getYAxisStringConverter() { + return new StringConverter<>() { + @Override + public String toString(Number value) { + return yAxisFormatter.apply(value); + } + + @Override + public Number fromString(String string) { + return null; + } + }; + } + + void setBsqUsdPriceFormatter() { + priceFormat.setMaximumFractionDigits(4); + yAxisFormatter = value -> priceFormat.format(value) + " BSQ/USD"; + } + + void setBsqBtcPriceFormatter() { + priceFormat.setMaximumFractionDigits(8); + yAxisFormatter = value -> { + value = MathUtils.scaleDownByPowerOf10(value.longValue(), 8); + return priceFormat.format(value) + " BSQ/BTC"; + }; + } + + void setBtcUsdPriceFormatter() { + priceFormat.setMaximumFractionDigits(0); + yAxisFormatter = value -> priceFormat.format(value) + " BTC/USD"; + } +} diff --git a/desktop/src/main/java/bisq/desktop/main/dao/economy/dashboard/volume/VolumeChartDataModel.java b/desktop/src/main/java/bisq/desktop/main/dao/economy/dashboard/volume/VolumeChartDataModel.java new file mode 100644 index 00000000000..4dc7feb7272 --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/main/dao/economy/dashboard/volume/VolumeChartDataModel.java @@ -0,0 +1,141 @@ +/* + * This file is part of Bisq. + * + * Bisq is free software: you can redistribute it and/or modify it + * under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at + * your option) any later version. + * + * Bisq is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public + * License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Bisq. If not, see . + */ + +package bisq.desktop.main.dao.economy.dashboard.volume; + +import bisq.desktop.components.chart.ChartDataModel; + +import bisq.core.trade.statistics.TradeStatistics3; +import bisq.core.trade.statistics.TradeStatisticsManager; + +import javax.inject.Inject; + +import java.time.Instant; + +import java.util.AbstractMap; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.function.Function; +import java.util.function.Predicate; +import java.util.stream.Collectors; + +import lombok.extern.slf4j.Slf4j; + +@Slf4j +public class VolumeChartDataModel extends ChartDataModel { + private final TradeStatisticsManager tradeStatisticsManager; + private Map usdVolumeByInterval, btcVolumeByInterval; + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Constructor + /////////////////////////////////////////////////////////////////////////////////////////// + + @Inject + public VolumeChartDataModel(TradeStatisticsManager tradeStatisticsManager) { + super(); + + this.tradeStatisticsManager = tradeStatisticsManager; + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Data + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + protected void invalidateCache() { + usdVolumeByInterval = null; + btcVolumeByInterval = null; + } + + public Map getUsdVolumeByInterval() { + if (usdVolumeByInterval != null) { + return usdVolumeByInterval; + } + + usdVolumeByInterval = getVolumeByInterval(VolumeChartDataModel::getVolumeInUsd); + return usdVolumeByInterval; + } + + public Map getBtcVolumeByInterval() { + if (btcVolumeByInterval != null) { + return btcVolumeByInterval; + } + + btcVolumeByInterval = getVolumeByInterval(VolumeChartDataModel::getVolumeInBtc); + return btcVolumeByInterval; + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Get volume functions + /////////////////////////////////////////////////////////////////////////////////////////// + + private static long getVolumeInUsd(List list) { + double sumBtcFromAllTrades = 0; + double sumBtcFromUsdTrades = 0; + double sumUsd = 0; + for (TradeStatistics3 tradeStatistics : list) { + long amount = tradeStatistics.getAmount(); + if (tradeStatistics.getCurrency().equals("USD")) { + sumUsd += tradeStatistics.getTradeVolume().getValue(); + sumBtcFromUsdTrades += amount; + } + sumBtcFromAllTrades += amount; + } + if (sumBtcFromAllTrades == 0 || sumBtcFromUsdTrades == 0 || sumUsd == 0) { + return 0L; + } + double averageUsdPrice = sumUsd / sumBtcFromUsdTrades; + // We truncate to 4 decimals + return (long) (sumBtcFromAllTrades * averageUsdPrice); + } + + private static long getVolumeInBtc(List list) { + return list.stream().mapToLong(TradeStatistics3::getAmount).sum(); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Aggregated collection data by interval + /////////////////////////////////////////////////////////////////////////////////////////// + + private Map getVolumeByInterval(Function, Long> getVolumeFunction) { + return getVolumeByInterval(tradeStatisticsManager.getObservableTradeStatisticsSet(), + tradeStatistics -> toTimeInterval(Instant.ofEpochMilli(tradeStatistics.getDateAsLong())), + dateFilter, + getVolumeFunction); + } + + private Map getVolumeByInterval(Collection collection, + Function groupByDateFunction, + Predicate dateFilter, + Function, Long> getVolumeFunction) { + return collection.stream() + .collect(Collectors.groupingBy(groupByDateFunction)) + .entrySet() + .stream() + .filter(entry -> dateFilter.test(entry.getKey())) + .map(entry -> new AbstractMap.SimpleEntry<>( + entry.getKey(), + getVolumeFunction.apply(entry.getValue()))) + .filter(e -> e.getValue() > 0L) + .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); + } +} diff --git a/desktop/src/main/java/bisq/desktop/main/dao/economy/dashboard/volume/VolumeChartView.java b/desktop/src/main/java/bisq/desktop/main/dao/economy/dashboard/volume/VolumeChartView.java new file mode 100644 index 00000000000..a18a3156347 --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/main/dao/economy/dashboard/volume/VolumeChartView.java @@ -0,0 +1,125 @@ +/* + * This file is part of Bisq. + * + * Bisq is free software: you can redistribute it and/or modify it + * under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at + * your option) any later version. + * + * Bisq is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public + * License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Bisq. If not, see . + */ + +package bisq.desktop.main.dao.economy.dashboard.volume; + +import bisq.desktop.components.chart.ChartView; + +import bisq.core.locale.Res; + +import javax.inject.Inject; + +import javafx.scene.chart.XYChart; + +import java.util.Collection; +import java.util.List; + +import lombok.extern.slf4j.Slf4j; + +@Slf4j +public class VolumeChartView extends ChartView { + private XYChart.Series seriesUsdVolume, seriesBtcVolume; + + @Inject + public VolumeChartView(VolumeChartViewModel model) { + super(model); + + setRadioButtonBehaviour(true); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Chart + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + protected void onSetYAxisFormatter(XYChart.Series series) { + if (series == seriesUsdVolume) { + model.setUsdVolumeFormatter(); + } else { + model.setBtcVolumeFormatter(); + } + } + + /////////////////////////////////////////////////////////////////////////////////////////// + // Legend + /////////////////////////////////////////////////////////////////////////////////////////// + + + @Override + protected Collection> getSeriesForLegend1() { + return List.of(seriesUsdVolume, seriesBtcVolume); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Timeline navigation + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + protected void initBoundsForTimelineNavigation() { + setBoundsForTimelineNavigation(seriesUsdVolume.getData()); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Series + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + protected void createSeries() { + seriesUsdVolume = new XYChart.Series<>(); + seriesUsdVolume.setName(Res.get("dao.factsAndFigures.supply.tradeVolumeInUsd")); + seriesIndexMap.put(getSeriesId(seriesUsdVolume), 0); + + seriesBtcVolume = new XYChart.Series<>(); + seriesBtcVolume.setName(Res.get("dao.factsAndFigures.supply.tradeVolumeInBtc")); + seriesIndexMap.put(getSeriesId(seriesBtcVolume), 1); + } + + @Override + protected void defineAndAddActiveSeries() { + activateSeries(seriesUsdVolume); + onSetYAxisFormatter(seriesUsdVolume); + } + + @Override + protected void activateSeries(XYChart.Series series) { + super.activateSeries(series); + + if (getSeriesId(series).equals(getSeriesId(seriesUsdVolume))) { + seriesUsdVolume.getData().setAll(model.getUsdVolumeChartData()); + } else if (getSeriesId(series).equals(getSeriesId(seriesBtcVolume))) { + seriesBtcVolume.getData().setAll(model.getBtcVolumeChartData()); + } + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Data + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + protected void applyData() { + if (activeSeries.contains(seriesUsdVolume)) { + seriesUsdVolume.getData().setAll(model.getUsdVolumeChartData()); + } + if (activeSeries.contains(seriesBtcVolume)) { + seriesBtcVolume.getData().setAll(model.getBtcVolumeChartData()); + } + } +} diff --git a/desktop/src/main/java/bisq/desktop/main/dao/economy/dashboard/volume/VolumeChartViewModel.java b/desktop/src/main/java/bisq/desktop/main/dao/economy/dashboard/volume/VolumeChartViewModel.java new file mode 100644 index 00000000000..5d914ab6c25 --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/main/dao/economy/dashboard/volume/VolumeChartViewModel.java @@ -0,0 +1,94 @@ +/* + * This file is part of Bisq. + * + * Bisq is free software: you can redistribute it and/or modify it + * under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at + * your option) any later version. + * + * Bisq is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public + * License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Bisq. If not, see . + */ + +package bisq.desktop.main.dao.economy.dashboard.volume; + +import bisq.desktop.components.chart.ChartViewModel; + +import bisq.core.locale.GlobalSettings; + +import bisq.common.util.MathUtils; + +import javax.inject.Inject; + +import javafx.scene.chart.XYChart; + +import javafx.util.StringConverter; + +import java.text.DecimalFormat; + +import java.util.List; +import java.util.function.Function; + +import lombok.extern.slf4j.Slf4j; + +@Slf4j +public class VolumeChartViewModel extends ChartViewModel { + private Function yAxisFormatter = value -> value + " USD"; + private final DecimalFormat volumeFormat; + + + @Inject + public VolumeChartViewModel(VolumeChartDataModel dataModel) { + super(dataModel); + + volumeFormat = (DecimalFormat) DecimalFormat.getNumberInstance(GlobalSettings.getLocale()); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Chart data + /////////////////////////////////////////////////////////////////////////////////////////// + + List> getUsdVolumeChartData() { + return toChartLongData(dataModel.getUsdVolumeByInterval()); + } + + List> getBtcVolumeChartData() { + return toChartLongData(dataModel.getBtcVolumeByInterval()); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Formatters/Converters + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + protected StringConverter getYAxisStringConverter() { + return new StringConverter<>() { + @Override + public String toString(Number value) { + return yAxisFormatter.apply(value); + } + + @Override + public Number fromString(String string) { + return null; + } + }; + } + + void setUsdVolumeFormatter() { + volumeFormat.setMaximumFractionDigits(0); + yAxisFormatter = value -> volumeFormat.format(MathUtils.scaleDownByPowerOf10(value.longValue(), 4)) + " USD"; + } + + void setBtcVolumeFormatter() { + volumeFormat.setMaximumFractionDigits(4); + yAxisFormatter = value -> volumeFormat.format(MathUtils.scaleDownByPowerOf10(value.longValue(), 8)) + " BTC"; + } +} diff --git a/desktop/src/main/java/bisq/desktop/main/dao/economy/supply/SupplyView.java b/desktop/src/main/java/bisq/desktop/main/dao/economy/supply/SupplyView.java index bcc5c33cdee..673fa57b788 100644 --- a/desktop/src/main/java/bisq/desktop/main/dao/economy/supply/SupplyView.java +++ b/desktop/src/main/java/bisq/desktop/main/dao/economy/supply/SupplyView.java @@ -20,9 +20,7 @@ import bisq.desktop.common.view.ActivatableView; import bisq.desktop.common.view.FxmlView; import bisq.desktop.components.TitledGroupBg; -import bisq.desktop.components.chart.ChartViewModel; -import bisq.desktop.main.dao.economy.supply.daodata.DaoChartDataModel; -import bisq.desktop.main.dao.economy.supply.daodata.DaoChartView; +import bisq.desktop.main.dao.economy.supply.dao.DaoChartView; import bisq.desktop.util.Layout; import bisq.core.dao.DaoFacade; @@ -49,18 +47,15 @@ import static bisq.desktop.util.FormBuilder.addTopLabelReadOnlyTextField; @FxmlView -public class SupplyView extends ActivatableView implements DaoStateListener, ChartViewModel.Listener { +public class SupplyView extends ActivatableView implements DaoStateListener { private final DaoFacade daoFacade; private final DaoChartView daoChartView; - // Shared model between SupplyView and RevenueChartModel - private final DaoChartDataModel daoChartDataModel; private final BsqFormatter bsqFormatter; private TextField genesisIssueAmountTextField, compRequestIssueAmountTextField, reimbursementAmountTextField, totalBurntBsqTradeFeeTextField, totalLockedUpAmountTextField, totalUnlockingAmountTextField, totalUnlockedAmountTextField, totalConfiscatedAmountTextField, totalProofOfBurnAmountTextField; private int gridRow = 0; - private long fromDate, toDate; /////////////////////////////////////////////////////////////////////////////////////////// @@ -70,19 +65,15 @@ public class SupplyView extends ActivatableView implements DaoSt @Inject private SupplyView(DaoFacade daoFacade, DaoChartView daoChartView, - DaoChartDataModel daoChartDataModel, BsqFormatter bsqFormatter) { this.daoFacade = daoFacade; this.daoChartView = daoChartView; - this.daoChartDataModel = daoChartDataModel; this.bsqFormatter = bsqFormatter; } @Override public void initialize() { - daoFacade.getTx(daoFacade.getGenesisTxId()).ifPresent(tx -> fromDate = tx.getTime()); - - createChart(); + createDaoChart(); createIssuedAndBurnedFields(); createLockedBsqFields(); } @@ -94,28 +85,12 @@ protected void activate() { updateWithBsqBlockChainData(); - daoChartView.activate(); - daoChartView.addListener(this); daoFacade.addBsqStateListener(this); } @Override protected void deactivate() { - daoChartView.removeListener(this); daoFacade.removeBsqStateListener(this); - daoChartView.deactivate(); - } - - - /////////////////////////////////////////////////////////////////////////////////////////// - // ChartModel.Listener - /////////////////////////////////////////////////////////////////////////////////////////// - - @Override - public void onDateFilterChanged(long fromDate, long toDate) { - this.fromDate = fromDate; - this.toDate = toDate; - updateEconomicsData(); } @@ -132,13 +107,15 @@ public void onParseBlockCompleteAfterBatchProcessing(Block block) { // Build UI /////////////////////////////////////////////////////////////////////////////////////////// - private void createChart() { - addTitledGroupBg(root, gridRow, 2, Res.get("dao.factsAndFigures.supply.issuedVsBurnt")); + private void createDaoChart() { + TitledGroupBg titledGroupBg = addTitledGroupBg(root, gridRow, 2, Res.get("dao.factsAndFigures.supply.issuedVsBurnt")); + titledGroupBg.getStyleClass().add("last"); // hides separator as we add a second TitledGroupBg + daoChartView.initialize(); + VBox chartContainer = daoChartView.getRoot(); AnchorPane chartPane = new AnchorPane(); chartPane.getStyleClass().add("chart-pane"); - VBox chartContainer = daoChartView.getRoot(); AnchorPane.setTopAnchor(chartContainer, 15d); AnchorPane.setBottomAnchor(chartContainer, 10d); AnchorPane.setLeftAnchor(chartContainer, 25d); @@ -148,15 +125,15 @@ private void createChart() { GridPane.setMargin(chartPane, new Insets(Layout.FIRST_ROW_DISTANCE, 0, 0, 0)); chartPane.getChildren().add(chartContainer); - this.root.getChildren().add(chartPane); + root.getChildren().add(chartPane); } private void createIssuedAndBurnedFields() { - TitledGroupBg titledGroupBg = addTitledGroupBg(root, ++gridRow, 3, Res.get("dao.factsAndFigures.supply.issued"), Layout.GROUP_DISTANCE); + TitledGroupBg titledGroupBg = addTitledGroupBg(root, ++gridRow, 3, Res.get("dao.factsAndFigures.supply.issued"), Layout.FLOATING_LABEL_DISTANCE); titledGroupBg.getStyleClass().add("last"); // hides separator as we add a second TitledGroupBg Tuple3 genesisAmountTuple = addTopLabelReadOnlyTextField(root, gridRow, - Res.get("dao.factsAndFigures.supply.genesisIssueAmount"), Layout.FIRST_ROW_AND_GROUP_DISTANCE); + Res.get("dao.factsAndFigures.supply.genesisIssueAmount"), Layout.COMPACT_FIRST_ROW_AND_COMPACT_GROUP_DISTANCE); genesisIssueAmountTextField = genesisAmountTuple.second; GridPane.setColumnSpan(genesisAmountTuple.third, 2); @@ -204,16 +181,16 @@ private void updateWithBsqBlockChainData() { private void updateEconomicsData() { // We use the supplyDataProvider to get the adjusted data with static historical data as well to use the same // monthly scoped data. - Coin issuedAmountFromCompRequests = Coin.valueOf(daoChartDataModel.getCompensationAmount(fromDate, getToDate())); + Coin issuedAmountFromCompRequests = Coin.valueOf(daoChartView.getCompensationAmount()); compRequestIssueAmountTextField.setText(bsqFormatter.formatAmountWithGroupSeparatorAndCode(issuedAmountFromCompRequests)); - Coin issuedAmountFromReimbursementRequests = Coin.valueOf(daoChartDataModel.getReimbursementAmount(fromDate, getToDate())); + Coin issuedAmountFromReimbursementRequests = Coin.valueOf(daoChartView.getReimbursementAmount()); reimbursementAmountTextField.setText(bsqFormatter.formatAmountWithGroupSeparatorAndCode(issuedAmountFromReimbursementRequests)); - Coin totalBurntTradeFee = Coin.valueOf(daoChartDataModel.getBsqTradeFeeAmount(fromDate, getToDate())); + Coin totalBurntTradeFee = Coin.valueOf(daoChartView.getBsqTradeFeeAmount()); totalBurntBsqTradeFeeTextField.setText(bsqFormatter.formatAmountWithGroupSeparatorAndCode(totalBurntTradeFee)); - Coin totalProofOfBurnAmount = Coin.valueOf(daoChartDataModel.getProofOfBurnAmount(fromDate, getToDate())); + Coin totalProofOfBurnAmount = Coin.valueOf(daoChartView.getProofOfBurnAmount()); totalProofOfBurnAmountTextField.setText(bsqFormatter.formatAmountWithGroupSeparatorAndCode(totalProofOfBurnAmount)); } @@ -230,8 +207,4 @@ private void updateLockedTxData() { Coin totalConfiscatedAmount = Coin.valueOf(daoFacade.getTotalAmountOfConfiscatedTxOutputs()); totalConfiscatedAmountTextField.setText(bsqFormatter.formatAmountWithGroupSeparatorAndCode(totalConfiscatedAmount)); } - - private long getToDate() { - return toDate > 0 ? toDate : System.currentTimeMillis(); - } } diff --git a/desktop/src/main/java/bisq/desktop/main/dao/economy/supply/daodata/DaoChartDataModel.java b/desktop/src/main/java/bisq/desktop/main/dao/economy/supply/dao/DaoChartDataModel.java similarity index 53% rename from desktop/src/main/java/bisq/desktop/main/dao/economy/supply/daodata/DaoChartDataModel.java rename to desktop/src/main/java/bisq/desktop/main/dao/economy/supply/dao/DaoChartDataModel.java index 75c11d9c3fb..52b90f72bab 100644 --- a/desktop/src/main/java/bisq/desktop/main/dao/economy/supply/daodata/DaoChartDataModel.java +++ b/desktop/src/main/java/bisq/desktop/main/dao/economy/supply/dao/DaoChartDataModel.java @@ -15,9 +15,9 @@ * along with Bisq. If not, see . */ -package bisq.desktop.main.dao.economy.supply.daodata; +package bisq.desktop.main.dao.economy.supply.dao; -import bisq.desktop.components.chart.TemporalAdjusterModel; +import bisq.desktop.components.chart.ChartDataModel; import bisq.core.dao.state.DaoStateService; import bisq.core.dao.state.model.blockchain.Tx; @@ -28,163 +28,190 @@ import javax.inject.Singleton; import java.time.Instant; -import java.time.temporal.TemporalAdjuster; import java.util.Collection; import java.util.HashMap; import java.util.Map; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; -import java.util.function.BinaryOperator; import java.util.function.Function; import java.util.function.Predicate; import java.util.stream.Collectors; -import java.util.stream.Stream; import lombok.extern.slf4j.Slf4j; @Slf4j @Singleton -public class DaoChartDataModel { +public class DaoChartDataModel extends ChartDataModel { private final DaoStateService daoStateService; private final Function blockTimeOfIssuanceFunction; - private final TemporalAdjusterModel temporalAdjusterModel; + private Map totalIssuedByInterval, compensationByInterval, reimbursementByInterval, + totalBurnedByInterval, bsqTradeFeeByInterval, proofOfBurnByInterval; + /////////////////////////////////////////////////////////////////////////////////////////// // Constructor /////////////////////////////////////////////////////////////////////////////////////////// @Inject - public DaoChartDataModel(DaoStateService daoStateService, TemporalAdjusterModel temporalAdjusterModel) { + public DaoChartDataModel(DaoStateService daoStateService) { super(); + this.daoStateService = daoStateService; + // TODO getBlockTime is the bottleneck. Add a lookup map to daoState to fix that in a dedicated PR. blockTimeOfIssuanceFunction = memoize(issuance -> { int height = daoStateService.getStartHeightOfCurrentCycle(issuance.getChainHeight()).orElse(0); return daoStateService.getBlockTime(height); }); - this.temporalAdjusterModel = temporalAdjusterModel; } + @Override + protected void invalidateCache() { + totalIssuedByInterval = null; + compensationByInterval = null; + reimbursementByInterval = null; + totalBurnedByInterval = null; + bsqTradeFeeByInterval = null; + proofOfBurnByInterval = null; - /////////////////////////////////////////////////////////////////////////////////////////// - // API - /////////////////////////////////////////////////////////////////////////////////////////// - - public void setTemporalAdjuster(TemporalAdjuster temporalAdjuster) { - temporalAdjusterModel.setTemporalAdjuster(temporalAdjuster); } - public TemporalAdjuster getTemporalAdjuster() { - return temporalAdjusterModel.getTemporalAdjuster(); - } - /** - * @param fromDate Epoch in millis - * @param toDate Epoch in millis - */ - public long getCompensationAmount(long fromDate, long toDate) { - return getMergedCompensationMap(temporalAdjusterModel.getPredicate(fromDate, toDate)).values().stream() + /////////////////////////////////////////////////////////////////////////////////////////// + // Total amounts + /////////////////////////////////////////////////////////////////////////////////////////// + + long getCompensationAmount() { + return getCompensationByInterval().values().stream() .mapToLong(e -> e) .sum(); } - /** - * @param fromDate Epoch in millis - * @param toDate Epoch in millis - */ - public long getReimbursementAmount(long fromDate, long toDate) { - return getMergedReimbursementMap(temporalAdjusterModel.getPredicate(fromDate, toDate)).values().stream() + long getReimbursementAmount() { + return getReimbursementByInterval().values().stream() .mapToLong(e -> e) .sum(); } - /** - * @param fromDate Epoch in millis - * @param toDate Epoch in millis - */ - public long getBsqTradeFeeAmount(long fromDate, long toDate) { - return getBurnedBsqByMonth(daoStateService.getTradeFeeTxs(), temporalAdjusterModel.getPredicate(fromDate, toDate)).values() - .stream() + long getBsqTradeFeeAmount() { + return getBsqTradeFeeByInterval().values().stream() .mapToLong(e -> e) .sum(); } - /** - * @param fromDate Epoch in millis - * @param toDate Epoch in millis - */ - public long getProofOfBurnAmount(long fromDate, long toDate) { - return getBurnedBsqByMonth(daoStateService.getProofOfBurnTxs(), temporalAdjusterModel.getPredicate(fromDate, toDate)).values().stream() + long getProofOfBurnAmount() { + return getProofOfBurnByInterval().values().stream() .mapToLong(e -> e) .sum(); } - public Map getBurnedBsqByMonth(Collection txs, Predicate predicate) { - return txs.stream() - .collect(Collectors.groupingBy(tx -> toTimeInterval(Instant.ofEpochMilli(tx.getTime())))) - .entrySet() - .stream() - .filter(entry -> predicate.test(entry.getKey())) - .collect(Collectors.toMap(Map.Entry::getKey, - entry -> entry.getValue().stream() - .mapToLong(Tx::getBurntBsq) - .sum())); + + /////////////////////////////////////////////////////////////////////////////////////////// + // Data for chart + /////////////////////////////////////////////////////////////////////////////////////////// + + Map getTotalIssuedByInterval() { + if (totalIssuedByInterval != null) { + return totalIssuedByInterval; + } + + Map compensationMap = getCompensationByInterval(); + Map reimbursementMap = getReimbursementByInterval(); + totalIssuedByInterval = getMergedMap(compensationMap, reimbursementMap, Long::sum); + return totalIssuedByInterval; } - // We map all issuance entries to a map with the beginning of the month as key and the list of issuance as value. - // Then we apply the date filter and and sum up all issuance amounts if the items in the list to return the - // issuance by month. We use calendar month because we want to combine the data with other data and using the cycle - // as adjuster would be more complicate (though could be done in future). - public Map getIssuedBsqByMonth(Set issuanceSet, Predicate predicate) { + Map getCompensationByInterval() { + if (compensationByInterval != null) { + return compensationByInterval; + } + + Set issuanceSetForType = daoStateService.getIssuanceSetForType(IssuanceType.COMPENSATION); + Map issuedBsqByInterval = getIssuedBsqByInterval(issuanceSetForType, getDateFilter()); + Map historicalIssuanceByInterval = getHistoricalIssuedBsqByInterval(DaoEconomyHistoricalData.COMPENSATIONS_BY_CYCLE_DATE, getDateFilter()); + compensationByInterval = getMergedMap(issuedBsqByInterval, historicalIssuanceByInterval, (daoDataValue, staticDataValue) -> staticDataValue); + return compensationByInterval; + } + + Map getReimbursementByInterval() { + if (reimbursementByInterval != null) { + return reimbursementByInterval; + } + + Map issuedBsqByInterval = getIssuedBsqByInterval(daoStateService.getIssuanceSetForType(IssuanceType.REIMBURSEMENT), getDateFilter()); + Map historicalIssuanceByInterval = getHistoricalIssuedBsqByInterval(DaoEconomyHistoricalData.REIMBURSEMENTS_BY_CYCLE_DATE, getDateFilter()); + reimbursementByInterval = getMergedMap(issuedBsqByInterval, historicalIssuanceByInterval, (daoDataValue, staticDataValue) -> staticDataValue); + return reimbursementByInterval; + } + + Map getTotalBurnedByInterval() { + if (totalBurnedByInterval != null) { + return totalBurnedByInterval; + } + + Map tradeFee = getBsqTradeFeeByInterval(); + Map proofOfBurn = getProofOfBurnByInterval(); + totalBurnedByInterval = getMergedMap(tradeFee, proofOfBurn, Long::sum); + return totalBurnedByInterval; + } + + Map getBsqTradeFeeByInterval() { + if (bsqTradeFeeByInterval != null) { + return bsqTradeFeeByInterval; + } + + bsqTradeFeeByInterval = getBurntBsqByInterval(daoStateService.getTradeFeeTxs(), getDateFilter()); + return bsqTradeFeeByInterval; + } + + Map getProofOfBurnByInterval() { + if (proofOfBurnByInterval != null) { + return proofOfBurnByInterval; + } + + proofOfBurnByInterval = getBurntBsqByInterval(daoStateService.getProofOfBurnTxs(), getDateFilter()); + return proofOfBurnByInterval; + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Aggregated collection data by interval + /////////////////////////////////////////////////////////////////////////////////////////// + + private Map getIssuedBsqByInterval(Set issuanceSet, Predicate dateFilter) { return issuanceSet.stream() .collect(Collectors.groupingBy(issuance -> toTimeInterval(Instant.ofEpochMilli(blockTimeOfIssuanceFunction.apply(issuance))))) .entrySet() .stream() - .filter(entry -> predicate.test(entry.getKey())) + .filter(entry -> dateFilter.test(entry.getKey())) .collect(Collectors.toMap(Map.Entry::getKey, entry -> entry.getValue().stream() .mapToLong(Issuance::getAmount) .sum())); } - public Map getMergedCompensationMap(Predicate predicate) { - Map issuedBsqByMonth = getIssuedBsqByMonth(daoStateService.getIssuanceSetForType(IssuanceType.COMPENSATION), predicate); - Map historicalIssuanceByMonth = getHistoricalIssuanceByMonth(DaoEconomyHistoricalData.COMPENSATIONS_BY_CYCLE_DATE, predicate); - return getMergedMap(issuedBsqByMonth, historicalIssuanceByMonth, (daoDataValue, staticDataValue) -> staticDataValue); - } - - public Map getMergedReimbursementMap(Predicate predicate) { - Map issuedBsqByMonth = getIssuedBsqByMonth(daoStateService.getIssuanceSetForType(IssuanceType.REIMBURSEMENT), predicate); - Map historicalIssuanceByMonth = getHistoricalIssuanceByMonth(DaoEconomyHistoricalData.REIMBURSEMENTS_BY_CYCLE_DATE, predicate); - return getMergedMap(issuedBsqByMonth, historicalIssuanceByMonth, (daoDataValue, staticDataValue) -> staticDataValue); - } + private Map getHistoricalIssuedBsqByInterval(Map historicalData, + Predicate dateFilter) { - public Map getMergedMap(Map map1, - Map map2, - BinaryOperator mergeFunction) { - return Stream.concat(map1.entrySet().stream(), - map2.entrySet().stream()) - .collect(Collectors.toMap(Map.Entry::getKey, - Map.Entry::getValue, - mergeFunction)); - } - - private Map getHistoricalIssuanceByMonth(Map historicalData, Predicate predicate) { - // We did not use the reimbursement requests initially (but the compensation requests) because the limits - // have been too low. Over time it got mixed in compensation requests and reimbursement requests. - // To reflect that we use static data derived from the Github data. For new data we do not need that anymore - // as we have clearly separated that now. In case we have duplicate data for a months we use the static data. return historicalData.entrySet().stream() - .filter(e -> predicate.test(e.getKey())) + .filter(e -> dateFilter.test(e.getKey())) .collect(Collectors.toMap(e -> toTimeInterval(Instant.ofEpochSecond(e.getKey())), Map.Entry::getValue, (a, b) -> a + b)); } - public long toTimeInterval(Instant instant) { - return temporalAdjusterModel.toTimeInterval(instant); + private Map getBurntBsqByInterval(Collection txs, Predicate dateFilter) { + return txs.stream() + .collect(Collectors.groupingBy(tx -> toTimeInterval(Instant.ofEpochMilli(tx.getTime())))) + .entrySet() + .stream() + .filter(entry -> dateFilter.test(entry.getKey())) + .collect(Collectors.toMap(Map.Entry::getKey, + entry -> entry.getValue().stream() + .mapToLong(Tx::getBurntBsq) + .sum())); } /////////////////////////////////////////////////////////////////////////////////////////// @@ -196,6 +223,15 @@ private static Function memoize(Function fn) { return x -> map.computeIfAbsent(x, fn); } + + /////////////////////////////////////////////////////////////////////////////////////////// + // Historical data + /////////////////////////////////////////////////////////////////////////////////////////// + + // We did not use the reimbursement requests initially (but the compensation requests) because the limits + // have been too low. Over time it got mixed in compensation requests and reimbursement requests. + // To reflect that we use static data derived from the Github data. For new data we do not need that anymore + // as we have clearly separated that now. In case we have duplicate data for a months we use the static data. private static class DaoEconomyHistoricalData { // Key is start date of the cycle in epoch seconds, value is reimbursement amount public final static Map REIMBURSEMENTS_BY_CYCLE_DATE = new HashMap<>(); diff --git a/desktop/src/main/java/bisq/desktop/main/dao/economy/supply/dao/DaoChartView.java b/desktop/src/main/java/bisq/desktop/main/dao/economy/supply/dao/DaoChartView.java new file mode 100644 index 00000000000..c31451570a6 --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/main/dao/economy/supply/dao/DaoChartView.java @@ -0,0 +1,173 @@ +/* + * This file is part of Bisq. + * + * Bisq is free software: you can redistribute it and/or modify it + * under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at + * your option) any later version. + * + * Bisq is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public + * License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Bisq. If not, see . + */ + +package bisq.desktop.main.dao.economy.supply.dao; + +import bisq.desktop.components.chart.ChartView; + +import bisq.core.locale.Res; + +import javax.inject.Inject; + +import javafx.scene.chart.XYChart; + +import java.util.Collection; +import java.util.List; + +import lombok.extern.slf4j.Slf4j; + +@Slf4j +public class DaoChartView extends ChartView { + private XYChart.Series seriesBsqTradeFee, seriesProofOfBurn, seriesCompensation, + seriesReimbursement, seriesTotalIssued, seriesTotalBurned; + + + @Inject + public DaoChartView(DaoChartViewModel model) { + super(model); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // API Total amounts + /////////////////////////////////////////////////////////////////////////////////////////// + + public long getCompensationAmount() { + return model.getCompensationAmount(); + } + + public long getReimbursementAmount() { + return model.getReimbursementAmount(); + } + + public long getBsqTradeFeeAmount() { + return model.getBsqTradeFeeAmount(); + } + + public long getProofOfBurnAmount() { + return model.getProofOfBurnAmount(); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Legend + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + protected Collection> getSeriesForLegend1() { + return List.of(seriesTotalIssued, seriesCompensation, seriesReimbursement); + } + + @Override + protected Collection> getSeriesForLegend2() { + return List.of(seriesTotalBurned, seriesBsqTradeFee, seriesProofOfBurn); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Timeline navigation + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + protected void initBoundsForTimelineNavigation() { + setBoundsForTimelineNavigation(seriesTotalBurned.getData()); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Series + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + protected void createSeries() { + seriesTotalIssued = new XYChart.Series<>(); + seriesTotalIssued.setName(Res.get("dao.factsAndFigures.supply.totalIssued")); + seriesIndexMap.put(getSeriesId(seriesTotalIssued), 0); + + seriesTotalBurned = new XYChart.Series<>(); + seriesTotalBurned.setName(Res.get("dao.factsAndFigures.supply.totalBurned")); + seriesIndexMap.put(getSeriesId(seriesTotalBurned), 1); + + seriesCompensation = new XYChart.Series<>(); + seriesCompensation.setName(Res.get("dao.factsAndFigures.supply.compReq")); + seriesIndexMap.put(getSeriesId(seriesCompensation), 2); + + seriesReimbursement = new XYChart.Series<>(); + seriesReimbursement.setName(Res.get("dao.factsAndFigures.supply.reimbursement")); + seriesIndexMap.put(getSeriesId(seriesReimbursement), 3); + + seriesBsqTradeFee = new XYChart.Series<>(); + seriesBsqTradeFee.setName(Res.get("dao.factsAndFigures.supply.bsqTradeFee")); + seriesIndexMap.put(getSeriesId(seriesBsqTradeFee), 4); + + seriesProofOfBurn = new XYChart.Series<>(); + seriesProofOfBurn.setName(Res.get("dao.factsAndFigures.supply.proofOfBurn")); + seriesIndexMap.put(getSeriesId(seriesProofOfBurn), 5); + } + + @Override + protected void defineAndAddActiveSeries() { + activateSeries(seriesTotalIssued); + activateSeries(seriesTotalBurned); + } + + @Override + protected void activateSeries(XYChart.Series series) { + super.activateSeries(series); + + if (getSeriesId(series).equals(getSeriesId(seriesBsqTradeFee))) { + seriesBsqTradeFee.getData().setAll(model.getBsqTradeFeeChartData()); + } else if (getSeriesId(series).equals(getSeriesId(seriesCompensation))) { + seriesCompensation.getData().setAll(model.getCompensationChartData()); + } else if (getSeriesId(series).equals(getSeriesId(seriesProofOfBurn))) { + seriesProofOfBurn.getData().setAll(model.getProofOfBurnChartData()); + } else if (getSeriesId(series).equals(getSeriesId(seriesReimbursement))) { + seriesReimbursement.getData().setAll(model.getReimbursementChartData()); + } else if (getSeriesId(series).equals(getSeriesId(seriesTotalIssued))) { + seriesTotalIssued.getData().setAll(model.getTotalIssuedChartData()); + } else if (getSeriesId(series).equals(getSeriesId(seriesTotalBurned))) { + seriesTotalBurned.getData().setAll(model.getTotalBurnedChartData()); + } + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Data + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + protected void applyData() { + if (activeSeries.contains(seriesTotalIssued)) { + seriesTotalIssued.getData().setAll(model.getTotalIssuedChartData()); + } + if (activeSeries.contains(seriesTotalBurned)) { + seriesTotalBurned.getData().setAll(model.getTotalBurnedChartData()); + } + if (activeSeries.contains(seriesCompensation)) { + seriesCompensation.getData().setAll(model.getCompensationChartData()); + } + if (activeSeries.contains(seriesReimbursement)) { + seriesReimbursement.getData().setAll(model.getReimbursementChartData()); + } + if (activeSeries.contains(seriesBsqTradeFee)) { + seriesBsqTradeFee.getData().setAll(model.getBsqTradeFeeChartData()); + } + if (activeSeries.contains(seriesProofOfBurn)) { + seriesProofOfBurn.getData().setAll(model.getProofOfBurnChartData()); + } + } +} diff --git a/desktop/src/main/java/bisq/desktop/main/dao/economy/supply/dao/DaoChartViewModel.java b/desktop/src/main/java/bisq/desktop/main/dao/economy/supply/dao/DaoChartViewModel.java new file mode 100644 index 00000000000..6acca876a18 --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/main/dao/economy/supply/dao/DaoChartViewModel.java @@ -0,0 +1,125 @@ +/* + * This file is part of Bisq. + * + * Bisq is free software: you can redistribute it and/or modify it + * under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at + * your option) any later version. + * + * Bisq is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public + * License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Bisq. If not, see . + */ + +package bisq.desktop.main.dao.economy.supply.dao; + +import bisq.desktop.components.chart.ChartViewModel; + +import bisq.core.locale.GlobalSettings; +import bisq.core.util.coin.BsqFormatter; + +import javax.inject.Inject; + +import javafx.scene.chart.XYChart; + +import javafx.util.StringConverter; + +import java.text.DecimalFormat; + +import java.util.List; + +import lombok.extern.slf4j.Slf4j; + +@Slf4j +public class DaoChartViewModel extends ChartViewModel { + private final DecimalFormat priceFormat; + private final BsqFormatter bsqFormatter; + + + @Inject + public DaoChartViewModel(DaoChartDataModel dataModel, BsqFormatter bsqFormatter) { + super(dataModel); + + this.bsqFormatter = bsqFormatter; + priceFormat = (DecimalFormat) DecimalFormat.getNumberInstance(GlobalSettings.getLocale()); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Chart data + /////////////////////////////////////////////////////////////////////////////////////////// + + List> getTotalIssuedChartData() { + return toChartData(dataModel.getTotalIssuedByInterval()); + } + + List> getCompensationChartData() { + return toChartData(dataModel.getCompensationByInterval()); + } + + List> getReimbursementChartData() { + return toChartData(dataModel.getReimbursementByInterval()); + } + + List> getTotalBurnedChartData() { + return toChartData(dataModel.getTotalBurnedByInterval()); + } + + List> getBsqTradeFeeChartData() { + return toChartData(dataModel.getBsqTradeFeeByInterval()); + } + + List> getProofOfBurnChartData() { + return toChartData(dataModel.getProofOfBurnByInterval()); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Formatters/Converters + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + protected StringConverter getYAxisStringConverter() { + return new StringConverter<>() { + @Override + public String toString(Number value) { + return priceFormat.format(Double.parseDouble(bsqFormatter.formatBSQSatoshis(value.longValue()))) + " BSQ"; + } + + @Override + public Number fromString(String string) { + return null; + } + }; + } + + @Override + protected String getTooltipValueConverter(Number value) { + return bsqFormatter.formatBSQSatoshisWithCode(value.longValue()); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // DaoChartDataModel delegates + /////////////////////////////////////////////////////////////////////////////////////////// + + long getCompensationAmount() { + return dataModel.getCompensationAmount(); + } + + long getReimbursementAmount() { + return dataModel.getReimbursementAmount(); + } + + long getBsqTradeFeeAmount() { + return dataModel.getBsqTradeFeeAmount(); + } + + long getProofOfBurnAmount() { + return dataModel.getProofOfBurnAmount(); + } +} diff --git a/desktop/src/main/java/bisq/desktop/main/dao/economy/supply/daodata/DaoChartView.java b/desktop/src/main/java/bisq/desktop/main/dao/economy/supply/daodata/DaoChartView.java deleted file mode 100644 index 7a31215cbe0..00000000000 --- a/desktop/src/main/java/bisq/desktop/main/dao/economy/supply/daodata/DaoChartView.java +++ /dev/null @@ -1,292 +0,0 @@ -/* - * This file is part of Bisq. - * - * Bisq is free software: you can redistribute it and/or modify it - * under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or (at - * your option) any later version. - * - * Bisq is distributed in the hope that it will be useful, but WITHOUT - * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or - * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public - * License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with Bisq. If not, see . - */ - -package bisq.desktop.main.dao.economy.supply.daodata; - -import bisq.desktop.components.chart.ChartView; -import bisq.desktop.util.DisplayUtils; - -import bisq.core.locale.Res; -import bisq.core.util.coin.BsqFormatter; - -import bisq.common.UserThread; - -import javax.inject.Inject; - -import javafx.scene.Node; -import javafx.scene.chart.LineChart; -import javafx.scene.chart.NumberAxis; -import javafx.scene.chart.XYChart; -import javafx.scene.text.Text; - -import javafx.geometry.Side; - -import javafx.collections.ListChangeListener; - -import javafx.util.StringConverter; - -import java.time.Instant; - -import java.text.DecimalFormat; - -import java.util.Collection; -import java.util.Date; -import java.util.List; -import java.util.function.Predicate; - -import lombok.extern.slf4j.Slf4j; - -@Slf4j -public class DaoChartView extends ChartView { - private static final DecimalFormat priceFormat = new DecimalFormat(",###"); - private final BsqFormatter bsqFormatter; - - private XYChart.Series seriesBsqTradeFee, seriesProofOfBurn, seriesCompensation, - seriesReimbursement, seriesTotalIssued, seriesTotalBurned; - private ListChangeListener nodeListChangeListener; - - @Inject - public DaoChartView(DaoChartViewModel model, BsqFormatter bsqFormatter) { - super(model); - - this.bsqFormatter = bsqFormatter; - } - - - /////////////////////////////////////////////////////////////////////////////////////////// - // Lifecycle - /////////////////////////////////////////////////////////////////////////////////////////// - - @Override - public void initialize() { - super.initialize(); - - // Turn off detail series - hideSeries(seriesBsqTradeFee); - hideSeries(seriesCompensation); - hideSeries(seriesProofOfBurn); - hideSeries(seriesReimbursement); - - nodeListChangeListener = c -> { - while (c.next()) { - if (c.wasAdded()) { - for (Node mark : c.getAddedSubList()) { - if (mark instanceof Text) { - mark.getStyleClass().add("axis-tick-mark-text-node"); - } - } - } - } - }; - } - - @Override - public void activate() { - super.activate(); - xAxis.getChildrenUnmodifiable().addListener(nodeListChangeListener); - } - - @Override - public void deactivate() { - super.deactivate(); - xAxis.getChildrenUnmodifiable().removeListener(nodeListChangeListener); - } - - - /////////////////////////////////////////////////////////////////////////////////////////// - // Customisations - /////////////////////////////////////////////////////////////////////////////////////////// - - @Override - protected NumberAxis getXAxis() { - NumberAxis xAxis = new NumberAxis(); - xAxis.setForceZeroInRange(false); - xAxis.setTickLabelFormatter(getTimeAxisStringConverter()); - return xAxis; - } - - @Override - protected NumberAxis getYAxis() { - NumberAxis yAxis = new NumberAxis(); - yAxis.setForceZeroInRange(false); - yAxis.setSide(Side.RIGHT); - yAxis.setTickLabelFormatter(getYAxisStringConverter()); - return yAxis; - } - - @Override - protected XYChart getChart() { - LineChart chart = new LineChart<>(xAxis, yAxis); - chart.setAnimated(false); - chart.setCreateSymbols(true); - chart.setLegendVisible(false); - chart.setId("charts-dao"); - return chart; - } - - @Override - protected StringConverter getTimeAxisStringConverter() { - return new StringConverter<>() { - @Override - public String toString(Number epochSeconds) { - Date date = new Date(model.toTimeInterval(Instant.ofEpochSecond(epochSeconds.longValue())) * 1000); - return DisplayUtils.formatDateAxis(date, dateFormatPatters); - } - - @Override - public Number fromString(String string) { - return 0; - } - }; - } - - @Override - protected StringConverter getYAxisStringConverter() { - return new StringConverter<>() { - @Override - public String toString(Number value) { - return priceFormat.format(Double.parseDouble(bsqFormatter.formatBSQSatoshis(value.longValue()))) + " BSQ"; - } - - @Override - public Number fromString(String string) { - return null; - } - }; - } - - @Override - protected String getTooltipValueConverter(Number value) { - return bsqFormatter.formatBSQSatoshisWithCode(value.longValue()); - } - - @Override - protected void initSeries() { - seriesTotalIssued = new XYChart.Series<>(); - seriesTotalIssued.setName(Res.get("dao.factsAndFigures.supply.totalIssued")); - seriesIndexMap.put(getSeriesId(seriesTotalIssued), 0); - - seriesTotalBurned = new XYChart.Series<>(); - seriesTotalBurned.setName(Res.get("dao.factsAndFigures.supply.totalBurned")); - seriesIndexMap.put(getSeriesId(seriesTotalBurned), 1); - - seriesCompensation = new XYChart.Series<>(); - seriesCompensation.setName(Res.get("dao.factsAndFigures.supply.compReq")); - seriesIndexMap.put(getSeriesId(seriesCompensation), 2); - - seriesReimbursement = new XYChart.Series<>(); - seriesReimbursement.setName(Res.get("dao.factsAndFigures.supply.reimbursement")); - seriesIndexMap.put(getSeriesId(seriesReimbursement), 3); - - seriesBsqTradeFee = new XYChart.Series<>(); - seriesBsqTradeFee.setName(Res.get("dao.factsAndFigures.supply.bsqTradeFee")); - seriesIndexMap.put(getSeriesId(seriesBsqTradeFee), 4); - - seriesProofOfBurn = new XYChart.Series<>(); - seriesProofOfBurn.setName(Res.get("dao.factsAndFigures.supply.proofOfBurn")); - seriesIndexMap.put(getSeriesId(seriesProofOfBurn), 5); - } - - @Override - protected void addActiveSeries() { - addSeries(seriesTotalIssued); - addSeries(seriesTotalBurned); - } - - private void addSeries(XYChart.Series series) { - activeSeries.add(getSeriesId(series)); - chart.getData().add(series); - } - - @Override - protected Collection> getSeriesForLegend1() { - return List.of(seriesTotalIssued, seriesCompensation, seriesReimbursement); - } - - @Override - protected Collection> getSeriesForLegend2() { - return List.of(seriesTotalBurned, seriesBsqTradeFee, seriesProofOfBurn); - } - - @Override - protected void initData() { - Predicate predicate = e -> true; - List> bsqTradeFeeChartData = model.getBsqTradeFeeChartData(predicate); - seriesBsqTradeFee.getData().setAll(bsqTradeFeeChartData); - - List> compensationRequestsChartData = model.getCompensationChartData(predicate); - seriesCompensation.getData().setAll(compensationRequestsChartData); - - // We don't need redundant data like reimbursementChartData as time value from compensationRequestsChartData - // will cover it - model.initBounds(bsqTradeFeeChartData, compensationRequestsChartData); - xAxis.setLowerBound(model.getLowerBound().doubleValue()); - xAxis.setUpperBound(model.getUpperBound().doubleValue()); - - updateOtherSeries(predicate); - - UserThread.execute(this::setTimeLineLabels); - } - - @Override - protected void updateData(Predicate predicate) { - if (activeSeries.contains(getSeriesId(seriesBsqTradeFee))) { - seriesBsqTradeFee.getData().setAll(model.getBsqTradeFeeChartData(predicate)); - } - if (activeSeries.contains(getSeriesId(seriesCompensation))) { - seriesCompensation.getData().setAll(model.getCompensationChartData(predicate)); - } - - updateOtherSeries(predicate); - - applyTooltip(); - applySeriesStyles(); - } - - @Override - protected void activateSeries(XYChart.Series series) { - super.activateSeries(series); - if (getSeriesId(series).equals(getSeriesId(seriesBsqTradeFee))) { - seriesBsqTradeFee.getData().setAll(model.getBsqTradeFeeChartData(model.getPredicate())); - } else if (getSeriesId(series).equals(getSeriesId(seriesCompensation))) { - seriesCompensation.getData().setAll(model.getCompensationChartData(model.getPredicate())); - } else if (getSeriesId(series).equals(getSeriesId(seriesProofOfBurn))) { - seriesProofOfBurn.getData().setAll(model.getProofOfBurnChartData(model.getPredicate())); - } else if (getSeriesId(series).equals(getSeriesId(seriesReimbursement))) { - seriesReimbursement.getData().setAll(model.getReimbursementChartData(model.getPredicate())); - } else if (getSeriesId(series).equals(getSeriesId(seriesTotalIssued))) { - seriesTotalIssued.getData().setAll(model.getTotalIssuedChartData(model.getPredicate())); - } else if (getSeriesId(series).equals(getSeriesId(seriesTotalBurned))) { - seriesTotalBurned.getData().setAll(model.getTotalBurnedChartData(model.getPredicate())); - } - } - - private void updateOtherSeries(Predicate predicate) { - if (activeSeries.contains(getSeriesId(seriesProofOfBurn))) { - seriesProofOfBurn.getData().setAll(model.getProofOfBurnChartData(predicate)); - } - if (activeSeries.contains(getSeriesId(seriesReimbursement))) { - seriesReimbursement.getData().setAll(model.getReimbursementChartData(predicate)); - } - if (activeSeries.contains(getSeriesId(seriesTotalIssued))) { - seriesTotalIssued.getData().setAll(model.getTotalIssuedChartData(predicate)); - } - if (activeSeries.contains(getSeriesId(seriesTotalBurned))) { - seriesTotalBurned.getData().setAll(model.getTotalBurnedChartData(predicate)); - } - } -} diff --git a/desktop/src/main/java/bisq/desktop/main/dao/economy/supply/daodata/DaoChartViewModel.java b/desktop/src/main/java/bisq/desktop/main/dao/economy/supply/daodata/DaoChartViewModel.java deleted file mode 100644 index f5e85bea0b6..00000000000 --- a/desktop/src/main/java/bisq/desktop/main/dao/economy/supply/daodata/DaoChartViewModel.java +++ /dev/null @@ -1,128 +0,0 @@ -/* - * This file is part of Bisq. - * - * Bisq is free software: you can redistribute it and/or modify it - * under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or (at - * your option) any later version. - * - * Bisq is distributed in the hope that it will be useful, but WITHOUT - * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or - * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public - * License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with Bisq. If not, see . - */ - -package bisq.desktop.main.dao.economy.supply.daodata; - -import bisq.desktop.components.chart.ChartViewModel; - -import bisq.core.dao.state.DaoStateService; - -import bisq.common.util.Tuple2; - -import javax.inject.Inject; - -import javafx.scene.chart.XYChart; - -import java.time.Instant; -import java.time.temporal.TemporalAdjuster; - -import java.util.List; -import java.util.Map; -import java.util.function.Predicate; -import java.util.stream.Collectors; - -import lombok.extern.slf4j.Slf4j; - -@Slf4j -public class DaoChartViewModel extends ChartViewModel { - private final DaoStateService daoStateService; - private final DaoChartDataModel daoChartDataModel; - - /////////////////////////////////////////////////////////////////////////////////////////// - // Constructor - /////////////////////////////////////////////////////////////////////////////////////////// - - @Inject - public DaoChartViewModel(DaoStateService daoStateService, DaoChartDataModel daoChartDataModel) { - super(); - - this.daoStateService = daoStateService; - this.daoChartDataModel = daoChartDataModel; - } - - @Override - protected void applyTemporalAdjuster(TemporalAdjuster temporalAdjuster) { - daoChartDataModel.setTemporalAdjuster(temporalAdjuster); - } - - @Override - protected TemporalAdjuster getTemporalAdjuster() { - return daoChartDataModel.getTemporalAdjuster(); - } - - List> getBsqTradeFeeChartData(Predicate predicate) { - return toChartData(daoChartDataModel.getBurnedBsqByMonth(daoStateService.getTradeFeeTxs(), predicate)); - } - - List> getCompensationChartData(Predicate predicate) { - return toChartData(daoChartDataModel.getMergedCompensationMap(predicate)); - } - - List> getProofOfBurnChartData(Predicate predicate) { - return toChartData(daoChartDataModel.getBurnedBsqByMonth(daoStateService.getProofOfBurnTxs(), predicate)); - } - - List> getReimbursementChartData(Predicate predicate) { - return toChartData(daoChartDataModel.getMergedReimbursementMap(predicate)); - } - - List> getTotalIssuedChartData(Predicate predicate) { - Map compensationMap = daoChartDataModel.getMergedCompensationMap(predicate); - Map reimbursementMap = daoChartDataModel.getMergedReimbursementMap(predicate); - Map sum = daoChartDataModel.getMergedMap(compensationMap, reimbursementMap, Long::sum); - return toChartData(sum); - } - - List> getTotalBurnedChartData(Predicate predicate) { - Map tradeFee = daoChartDataModel.getBurnedBsqByMonth(daoStateService.getTradeFeeTxs(), predicate); - Map proofOfBurn = daoChartDataModel.getBurnedBsqByMonth(daoStateService.getProofOfBurnTxs(), predicate); - Map sum = daoChartDataModel.getMergedMap(tradeFee, proofOfBurn, Long::sum); - return toChartData(sum); - } - - void initBounds(List> tradeFeeChartData, - List> compensationRequestsChartData) { - Tuple2 xMinMaxTradeFee = getMinMax(tradeFeeChartData); - Tuple2 xMinMaxCompensationRequest = getMinMax(compensationRequestsChartData); - - lowerBound = Math.min(xMinMaxTradeFee.first, xMinMaxCompensationRequest.first); - upperBound = Math.max(xMinMaxTradeFee.second, xMinMaxCompensationRequest.second); - } - - long toTimeInterval(Instant ofEpochSecond) { - return daoChartDataModel.toTimeInterval(ofEpochSecond); - } - - /////////////////////////////////////////////////////////////////////////////////////////// - // Utils - /////////////////////////////////////////////////////////////////////////////////////////// - - private static List> toChartData(Map map) { - return map.entrySet().stream() - .map(entry -> new XYChart.Data(entry.getKey(), entry.getValue())) - .collect(Collectors.toList()); - } - - private static Tuple2 getMinMax(List> chartData) { - long min = Long.MAX_VALUE, max = 0; - for (XYChart.Data data : chartData) { - min = Math.min(data.getXValue().longValue(), min); - max = Math.max(data.getXValue().longValue(), max); - } - return new Tuple2<>((double) min, (double) max); - } -} diff --git a/desktop/src/main/java/bisq/desktop/theme-dark.css b/desktop/src/main/java/bisq/desktop/theme-dark.css index 42c8471ebad..5a54909171d 100644 --- a/desktop/src/main/java/bisq/desktop/theme-dark.css +++ b/desktop/src/main/java/bisq/desktop/theme-dark.css @@ -136,7 +136,7 @@ -bs-prompt-text: -bs-color-gray-3; -bs-decimals: #db6300; -bs-soft-red: #aa4c3b; - -bs-turquoise-light: #00dcdd; + -bs-turquoise-light: #11eeee; /* dao chart colors */ -bs-chart-dao-line1: -bs-color-blue-5; diff --git a/desktop/src/main/java/bisq/desktop/theme-light.css b/desktop/src/main/java/bisq/desktop/theme-light.css index 678e681e823..4688387ddf6 100644 --- a/desktop/src/main/java/bisq/desktop/theme-light.css +++ b/desktop/src/main/java/bisq/desktop/theme-light.css @@ -103,7 +103,7 @@ -bs-white: white; -bs-prompt-text: -fx-control-inner-background; -bs-soft-red: #aa4c3b; - -bs-turquoise-light: #00dcdd; + -bs-turquoise-light: #11eeee; /* dao chart colors */ -bs-chart-dao-line1: -bs-color-blue-5; From 25f35fea9e1b143273ae79f95588693f7b686a56 Mon Sep 17 00:00:00 2001 From: chimp1984 Date: Tue, 9 Feb 2021 18:56:49 -0500 Subject: [PATCH 15/21] Update text field values when time interval selection changes --- .../resources/i18n/displayStrings.properties | 2 +- .../main/dao/economy/supply/SupplyView.java | 63 ++++++----- .../dao/economy/supply/dao/DaoChartView.java | 105 ++++++++++++++---- 3 files changed, 115 insertions(+), 55 deletions(-) diff --git a/core/src/main/resources/i18n/displayStrings.properties b/core/src/main/resources/i18n/displayStrings.properties index f40f3d628a2..7889f156072 100644 --- a/core/src/main/resources/i18n/displayStrings.properties +++ b/core/src/main/resources/i18n/displayStrings.properties @@ -2443,7 +2443,7 @@ dao.factsAndFigures.supply.reimbursement=Reimbursement requests dao.factsAndFigures.supply.genesisIssueAmount=BSQ issued at genesis transaction dao.factsAndFigures.supply.compRequestIssueAmount=BSQ issued for compensation requests dao.factsAndFigures.supply.reimbursementAmount=BSQ issued for reimbursement requests -dao.factsAndFigures.supply.totalIssued=Total issues BSQ +dao.factsAndFigures.supply.totalIssued=Total issued BSQ dao.factsAndFigures.supply.totalBurned=Total burned BSQ dao.factsAndFigures.supply.chart.tradeFee.toolTip={0}\n{1} dao.factsAndFigures.supply.burnt=BSQ burnt diff --git a/desktop/src/main/java/bisq/desktop/main/dao/economy/supply/SupplyView.java b/desktop/src/main/java/bisq/desktop/main/dao/economy/supply/SupplyView.java index 673fa57b788..c48316823ec 100644 --- a/desktop/src/main/java/bisq/desktop/main/dao/economy/supply/SupplyView.java +++ b/desktop/src/main/java/bisq/desktop/main/dao/economy/supply/SupplyView.java @@ -43,6 +43,9 @@ import javafx.geometry.Insets; +import javafx.beans.binding.Bindings; +import javafx.beans.property.ReadOnlyLongProperty; + import static bisq.desktop.util.FormBuilder.addTitledGroupBg; import static bisq.desktop.util.FormBuilder.addTopLabelReadOnlyTextField; @@ -52,9 +55,9 @@ public class SupplyView extends ActivatableView implements DaoSt private final DaoChartView daoChartView; private final BsqFormatter bsqFormatter; - private TextField genesisIssueAmountTextField, compRequestIssueAmountTextField, reimbursementAmountTextField, - totalBurntBsqTradeFeeTextField, totalLockedUpAmountTextField, totalUnlockingAmountTextField, - totalUnlockedAmountTextField, totalConfiscatedAmountTextField, totalProofOfBurnAmountTextField; + private TextField genesisIssueAmountTextField, compensationAmountTextField, reimbursementAmountTextField, + bsqTradeFeeAmountTextField, totalLockedUpAmountTextField, totalUnlockingAmountTextField, + totalUnlockedAmountTextField, totalConfiscatedAmountTextField, proofOfBurnAmountTextField; private int gridRow = 0; @@ -80,17 +83,34 @@ public void initialize() { @Override protected void activate() { + daoFacade.addBsqStateListener(this); + + compensationAmountTextField.textProperty().bind(Bindings.createStringBinding( + () -> getFormattedValue(daoChartView.compensationAmountProperty()), + daoChartView.compensationAmountProperty())); + reimbursementAmountTextField.textProperty().bind(Bindings.createStringBinding( + () -> getFormattedValue(daoChartView.reimbursementAmountProperty()), + daoChartView.reimbursementAmountProperty())); + bsqTradeFeeAmountTextField.textProperty().bind(Bindings.createStringBinding( + () -> getFormattedValue(daoChartView.bsqTradeFeeAmountProperty()), + daoChartView.bsqTradeFeeAmountProperty())); + proofOfBurnAmountTextField.textProperty().bind(Bindings.createStringBinding( + () -> getFormattedValue(daoChartView.proofOfBurnAmountProperty()), + daoChartView.proofOfBurnAmountProperty())); + Coin issuedAmountFromGenesis = daoFacade.getGenesisTotalSupply(); genesisIssueAmountTextField.setText(bsqFormatter.formatAmountWithGroupSeparatorAndCode(issuedAmountFromGenesis)); - updateWithBsqBlockChainData(); - - daoFacade.addBsqStateListener(this); } @Override protected void deactivate() { daoFacade.removeBsqStateListener(this); + + compensationAmountTextField.textProperty().unbind(); + reimbursementAmountTextField.textProperty().unbind(); + bsqTradeFeeAmountTextField.textProperty().unbind(); + proofOfBurnAmountTextField.textProperty().unbind(); } @@ -137,17 +157,17 @@ private void createIssuedAndBurnedFields() { genesisIssueAmountTextField = genesisAmountTuple.second; GridPane.setColumnSpan(genesisAmountTuple.third, 2); - compRequestIssueAmountTextField = addTopLabelReadOnlyTextField(root, ++gridRow, + compensationAmountTextField = addTopLabelReadOnlyTextField(root, ++gridRow, Res.get("dao.factsAndFigures.supply.compRequestIssueAmount")).second; reimbursementAmountTextField = addTopLabelReadOnlyTextField(root, gridRow, 1, Res.get("dao.factsAndFigures.supply.reimbursementAmount")).second; addTitledGroupBg(root, ++gridRow, 1, Res.get("dao.factsAndFigures.supply.burnt"), Layout.GROUP_DISTANCE_WITHOUT_SEPARATOR); - totalBurntBsqTradeFeeTextField = addTopLabelReadOnlyTextField(root, gridRow, + bsqTradeFeeAmountTextField = addTopLabelReadOnlyTextField(root, gridRow, Res.get("dao.factsAndFigures.supply.bsqTradeFee"), Layout.COMPACT_FIRST_ROW_AND_COMPACT_GROUP_DISTANCE).second; - totalProofOfBurnAmountTextField = addTopLabelReadOnlyTextField(root, gridRow, 1, + proofOfBurnAmountTextField = addTopLabelReadOnlyTextField(root, gridRow, 1, Res.get("dao.factsAndFigures.supply.proofOfBurn"), Layout.COMPACT_FIRST_ROW_AND_COMPACT_GROUP_DISTANCE).second; } @@ -174,27 +194,6 @@ private void createLockedBsqFields() { /////////////////////////////////////////////////////////////////////////////////////////// private void updateWithBsqBlockChainData() { - updateEconomicsData(); - updateLockedTxData(); - } - - private void updateEconomicsData() { - // We use the supplyDataProvider to get the adjusted data with static historical data as well to use the same - // monthly scoped data. - Coin issuedAmountFromCompRequests = Coin.valueOf(daoChartView.getCompensationAmount()); - compRequestIssueAmountTextField.setText(bsqFormatter.formatAmountWithGroupSeparatorAndCode(issuedAmountFromCompRequests)); - - Coin issuedAmountFromReimbursementRequests = Coin.valueOf(daoChartView.getReimbursementAmount()); - reimbursementAmountTextField.setText(bsqFormatter.formatAmountWithGroupSeparatorAndCode(issuedAmountFromReimbursementRequests)); - - Coin totalBurntTradeFee = Coin.valueOf(daoChartView.getBsqTradeFeeAmount()); - totalBurntBsqTradeFeeTextField.setText(bsqFormatter.formatAmountWithGroupSeparatorAndCode(totalBurntTradeFee)); - - Coin totalProofOfBurnAmount = Coin.valueOf(daoChartView.getProofOfBurnAmount()); - totalProofOfBurnAmountTextField.setText(bsqFormatter.formatAmountWithGroupSeparatorAndCode(totalProofOfBurnAmount)); - } - - private void updateLockedTxData() { Coin totalLockedUpAmount = Coin.valueOf(daoFacade.getTotalLockupAmount()); totalLockedUpAmountTextField.setText(bsqFormatter.formatAmountWithGroupSeparatorAndCode(totalLockedUpAmount)); @@ -207,4 +206,8 @@ private void updateLockedTxData() { Coin totalConfiscatedAmount = Coin.valueOf(daoFacade.getTotalAmountOfConfiscatedTxOutputs()); totalConfiscatedAmountTextField.setText(bsqFormatter.formatAmountWithGroupSeparatorAndCode(totalConfiscatedAmount)); } + + private String getFormattedValue(ReadOnlyLongProperty property) { + return bsqFormatter.formatAmountWithGroupSeparatorAndCode(Coin.valueOf(property.get())); + } } diff --git a/desktop/src/main/java/bisq/desktop/main/dao/economy/supply/dao/DaoChartView.java b/desktop/src/main/java/bisq/desktop/main/dao/economy/supply/dao/DaoChartView.java index c31451570a6..9e0e3dfbab6 100644 --- a/desktop/src/main/java/bisq/desktop/main/dao/economy/supply/dao/DaoChartView.java +++ b/desktop/src/main/java/bisq/desktop/main/dao/economy/supply/dao/DaoChartView.java @@ -25,6 +25,10 @@ import javafx.scene.chart.XYChart; +import javafx.beans.property.LongProperty; +import javafx.beans.property.ReadOnlyLongProperty; +import javafx.beans.property.SimpleLongProperty; + import java.util.Collection; import java.util.List; @@ -32,6 +36,11 @@ @Slf4j public class DaoChartView extends ChartView { + private LongProperty compensationAmountProperty = new SimpleLongProperty(); + private LongProperty reimbursementAmountProperty = new SimpleLongProperty(); + private LongProperty bsqTradeFeeAmountProperty = new SimpleLongProperty(); + private LongProperty proofOfBurnAmountProperty = new SimpleLongProperty(); + private XYChart.Series seriesBsqTradeFee, seriesProofOfBurn, seriesCompensation, seriesReimbursement, seriesTotalIssued, seriesTotalBurned; @@ -46,20 +55,36 @@ public DaoChartView(DaoChartViewModel model) { // API Total amounts /////////////////////////////////////////////////////////////////////////////////////////// - public long getCompensationAmount() { + public long getCompensationAmountProperty() { return model.getCompensationAmount(); } - public long getReimbursementAmount() { - return model.getReimbursementAmount(); + public ReadOnlyLongProperty compensationAmountProperty() { + if (compensationAmountProperty.get() == 0) { + compensationAmountProperty.set(model.getCompensationAmount()); + } + return compensationAmountProperty; } - public long getBsqTradeFeeAmount() { - return model.getBsqTradeFeeAmount(); + public ReadOnlyLongProperty reimbursementAmountProperty() { + if (reimbursementAmountProperty.get() == 0) { + reimbursementAmountProperty.set(model.getReimbursementAmount()); + } + return reimbursementAmountProperty; } - public long getProofOfBurnAmount() { - return model.getProofOfBurnAmount(); + public ReadOnlyLongProperty bsqTradeFeeAmountProperty() { + if (bsqTradeFeeAmountProperty.get() == 0) { + bsqTradeFeeAmountProperty.set(model.getBsqTradeFeeAmount()); + } + return bsqTradeFeeAmountProperty; + } + + public ReadOnlyLongProperty proofOfBurnAmountProperty() { + if (proofOfBurnAmountProperty.get() == 0) { + proofOfBurnAmountProperty.set(model.getProofOfBurnAmount()); + } + return proofOfBurnAmountProperty; } @@ -129,18 +154,18 @@ protected void defineAndAddActiveSeries() { protected void activateSeries(XYChart.Series series) { super.activateSeries(series); - if (getSeriesId(series).equals(getSeriesId(seriesBsqTradeFee))) { - seriesBsqTradeFee.getData().setAll(model.getBsqTradeFeeChartData()); + if (getSeriesId(series).equals(getSeriesId(seriesTotalIssued))) { + applyTotalIssued(); } else if (getSeriesId(series).equals(getSeriesId(seriesCompensation))) { - seriesCompensation.getData().setAll(model.getCompensationChartData()); - } else if (getSeriesId(series).equals(getSeriesId(seriesProofOfBurn))) { - seriesProofOfBurn.getData().setAll(model.getProofOfBurnChartData()); + applyCompensation(); } else if (getSeriesId(series).equals(getSeriesId(seriesReimbursement))) { - seriesReimbursement.getData().setAll(model.getReimbursementChartData()); - } else if (getSeriesId(series).equals(getSeriesId(seriesTotalIssued))) { - seriesTotalIssued.getData().setAll(model.getTotalIssuedChartData()); + applyReimbursement(); } else if (getSeriesId(series).equals(getSeriesId(seriesTotalBurned))) { - seriesTotalBurned.getData().setAll(model.getTotalBurnedChartData()); + applyTotalBurned(); + } else if (getSeriesId(series).equals(getSeriesId(seriesBsqTradeFee))) { + applyBsqTradeFee(); + } else if (getSeriesId(series).equals(getSeriesId(seriesProofOfBurn))) { + applyProofOfBurn(); } } @@ -152,22 +177,54 @@ protected void activateSeries(XYChart.Series series) { @Override protected void applyData() { if (activeSeries.contains(seriesTotalIssued)) { - seriesTotalIssued.getData().setAll(model.getTotalIssuedChartData()); - } - if (activeSeries.contains(seriesTotalBurned)) { - seriesTotalBurned.getData().setAll(model.getTotalBurnedChartData()); + applyTotalIssued(); } if (activeSeries.contains(seriesCompensation)) { - seriesCompensation.getData().setAll(model.getCompensationChartData()); + applyCompensation(); } if (activeSeries.contains(seriesReimbursement)) { - seriesReimbursement.getData().setAll(model.getReimbursementChartData()); + applyReimbursement(); + } + if (activeSeries.contains(seriesTotalBurned)) { + applyTotalBurned(); } if (activeSeries.contains(seriesBsqTradeFee)) { - seriesBsqTradeFee.getData().setAll(model.getBsqTradeFeeChartData()); + applyBsqTradeFee(); } if (activeSeries.contains(seriesProofOfBurn)) { - seriesProofOfBurn.getData().setAll(model.getProofOfBurnChartData()); + applyProofOfBurn(); } } + + private void applyTotalIssued() { + seriesTotalIssued.getData().setAll(model.getTotalIssuedChartData()); + compensationAmountProperty.set(model.getCompensationAmount()); + reimbursementAmountProperty.set(model.getReimbursementAmount()); + } + + private void applyCompensation() { + seriesCompensation.getData().setAll(model.getCompensationChartData()); + compensationAmountProperty.set(model.getCompensationAmount()); + } + + private void applyReimbursement() { + seriesReimbursement.getData().setAll(model.getReimbursementChartData()); + reimbursementAmountProperty.set(model.getReimbursementAmount()); + } + + private void applyTotalBurned() { + seriesTotalBurned.getData().setAll(model.getTotalBurnedChartData()); + bsqTradeFeeAmountProperty.set(model.getBsqTradeFeeAmount()); + proofOfBurnAmountProperty.set(model.getProofOfBurnAmount()); + } + + private void applyBsqTradeFee() { + seriesBsqTradeFee.getData().setAll(model.getBsqTradeFeeChartData()); + bsqTradeFeeAmountProperty.set(model.getBsqTradeFeeAmount()); + } + + private void applyProofOfBurn() { + seriesProofOfBurn.getData().setAll(model.getProofOfBurnChartData()); + proofOfBurnAmountProperty.set(model.getProofOfBurnAmount()); + } } From 955c57cfbe6c6b93a6c4753b162bfdffc383c444 Mon Sep 17 00:00:00 2001 From: chimp1984 Date: Tue, 9 Feb 2021 19:46:29 -0500 Subject: [PATCH 16/21] Reduce vertical space --- .../desktop/components/chart/ChartView.java | 51 ++++++++----------- .../economy/dashboard/BsqDashboardView.java | 11 ++-- .../main/dao/economy/supply/SupplyView.java | 2 +- 3 files changed, 28 insertions(+), 36 deletions(-) diff --git a/desktop/src/main/java/bisq/desktop/components/chart/ChartView.java b/desktop/src/main/java/bisq/desktop/components/chart/ChartView.java index 40484e71655..cb63032929a 100644 --- a/desktop/src/main/java/bisq/desktop/components/chart/ChartView.java +++ b/desktop/src/main/java/bisq/desktop/components/chart/ChartView.java @@ -24,7 +24,6 @@ import bisq.core.locale.Res; import bisq.common.UserThread; -import bisq.common.util.Tuple2; import javafx.scene.Node; import javafx.scene.chart.Axis; @@ -38,7 +37,6 @@ import javafx.scene.control.ToggleGroup; import javafx.scene.control.Tooltip; import javafx.scene.input.MouseEvent; -import javafx.scene.layout.AnchorPane; import javafx.scene.layout.HBox; import javafx.scene.layout.Pane; import javafx.scene.layout.Priority; @@ -74,8 +72,6 @@ import javax.annotation.Nullable; -import static bisq.desktop.util.FormBuilder.getTopLabelWithVBox; - @Slf4j public abstract class ChartView> extends ActivatableViewAndModel { private Pane center; @@ -134,7 +130,7 @@ public void initialize() { createSeries(); // Time interval - Pane timeIntervalBox = getTimeIntervalBox(); + HBox timeIntervalBox = getTimeIntervalBox(); // chart xAxis = getXAxis(); @@ -156,20 +152,19 @@ public void initialize() { defineAndAddActiveSeries(); // Put all together - VBox timelineNavigationBox = new VBox(); - double paddingLeft = 15; double paddingRight = 89; // Y-axis width depends on data so we register a listener to get correct value yAxisWidthListener = (observable, oldValue, newValue) -> { double width = newValue.doubleValue(); if (width > 0) { - double paddingRight1 = width + 14; - VBox.setMargin(timelineNavigation, new Insets(0, paddingRight1, 0, paddingLeft)); - VBox.setMargin(timelineLabels, new Insets(0, paddingRight1, 0, paddingLeft)); - VBox.setMargin(legendBox1, new Insets(10, paddingRight1, 0, paddingLeft)); + double rightPadding = width + 14; + VBox.setMargin(timeIntervalBox, new Insets(0, rightPadding, 0, paddingLeft)); + VBox.setMargin(timelineNavigation, new Insets(0, rightPadding, 0, paddingLeft)); + VBox.setMargin(timelineLabels, new Insets(0, rightPadding, 0, paddingLeft)); + VBox.setMargin(legendBox1, new Insets(10, rightPadding, 0, paddingLeft)); if (legendBox2 != null) { - VBox.setMargin(legendBox2, new Insets(-20, paddingRight1, 0, paddingLeft)); + VBox.setMargin(legendBox2, new Insets(-20, rightPadding, 0, paddingLeft)); } if (model.getDividerPositions()[0] == 0 && model.getDividerPositions()[1] == 1) { @@ -177,16 +172,15 @@ public void initialize() { } } }; - + VBox.setMargin(timeIntervalBox, new Insets(0, paddingRight, 0, paddingLeft)); VBox.setMargin(timelineNavigation, new Insets(0, paddingRight, 0, paddingLeft)); VBox.setMargin(timelineLabels, new Insets(0, paddingRight, 0, paddingLeft)); - VBox.setMargin(legendBox1, new Insets(10, paddingRight, 0, paddingLeft)); - timelineNavigationBox.getChildren().addAll(timelineNavigation, timelineLabels, legendBox1); + VBox.setMargin(legendBox1, new Insets(0, paddingRight, 0, paddingLeft)); + root.getChildren().addAll(timeIntervalBox, chart, timelineNavigation, timelineLabels, legendBox1); if (legendBox2 != null) { VBox.setMargin(legendBox2, new Insets(-20, paddingRight, 0, paddingLeft)); - timelineNavigationBox.getChildren().add(legendBox2); + root.getChildren().add(legendBox2); } - root.getChildren().addAll(timeIntervalBox, chart, timelineNavigationBox); // Listeners widthListener = (observable, oldValue, newValue) -> { @@ -271,7 +265,7 @@ public void deactivate() { // TimeInterval/TemporalAdjuster /////////////////////////////////////////////////////////////////////////////////////////// - protected Pane getTimeIntervalBox() { + protected HBox getTimeIntervalBox() { ToggleButton year = getTimeIntervalToggleButton(Res.get("time.year"), TemporalAdjusterModel.Interval.YEAR, timeIntervalToggleGroup, "toggle-left"); ToggleButton month = getTimeIntervalToggleButton(Res.get("time.month"), TemporalAdjusterModel.Interval.MONTH, @@ -280,18 +274,13 @@ protected Pane getTimeIntervalBox() { timeIntervalToggleGroup, "toggle-center"); ToggleButton day = getTimeIntervalToggleButton(Res.get("time.day"), TemporalAdjusterModel.Interval.DAY, timeIntervalToggleGroup, "toggle-center"); - HBox toggleBox = new HBox(); toggleBox.setSpacing(0); toggleBox.setAlignment(Pos.CENTER_LEFT); - toggleBox.getChildren().addAll(year, month, week, day); - - Tuple2 topLabelWithVBox = getTopLabelWithVBox(Res.get("shared.interval"), toggleBox); - AnchorPane pane = new AnchorPane(); - VBox vBox = topLabelWithVBox.second; - pane.getChildren().add(vBox); - AnchorPane.setRightAnchor(vBox, 90d); - return pane; + Region spacer = new Region(); + HBox.setHgrow(spacer, Priority.ALWAYS); + toggleBox.getChildren().addAll(spacer, year, month, week, day); + return toggleBox; } private ToggleButton getTimeIntervalToggleButton(String label, @@ -342,6 +331,7 @@ protected LineChart getChart() { chart.setAnimated(false); chart.setCreateSymbols(true); chart.setLegendVisible(false); + chart.setMinHeight(200); chart.setId("charts-dao"); return chart; } @@ -396,7 +386,7 @@ private void addTimelineNavigation() { center.setId("chart-navigation-center-pane"); Pane right = new Pane(); timelineNavigation = new SplitPane(); - timelineNavigation.setMinHeight(30); + timelineNavigation.setMinHeight(25); timelineNavigation.getItems().addAll(left, center, right); timelineLabels = new HBox(); } @@ -404,9 +394,10 @@ private void addTimelineNavigation() { // After initial chart data are created we apply the text from the x-axis ticks to our timeline navigation. protected void applyTimeLineNavigationLabels() { timelineLabels.getChildren().clear(); - int size = xAxis.getTickMarks().size(); + ObservableList> tickMarks = xAxis.getTickMarks(); + int size = tickMarks.size(); for (int i = 0; i < size; i++) { - Axis.TickMark tickMark = xAxis.getTickMarks().get(i); + Axis.TickMark tickMark = tickMarks.get(i); Number xValue = tickMark.getValue(); String xValueString; if (xAxis.getTickLabelFormatter() != null) { diff --git a/desktop/src/main/java/bisq/desktop/main/dao/economy/dashboard/BsqDashboardView.java b/desktop/src/main/java/bisq/desktop/main/dao/economy/dashboard/BsqDashboardView.java index 152cf23bbb3..8ac6b49f7a8 100644 --- a/desktop/src/main/java/bisq/desktop/main/dao/economy/dashboard/BsqDashboardView.java +++ b/desktop/src/main/java/bisq/desktop/main/dao/economy/dashboard/BsqDashboardView.java @@ -165,10 +165,10 @@ private void createTextFields() { marketPriceBox.second.getStyleClass().add("dao-kpi-subtext"); avgUSDPrice90TextField = addTopLabelReadOnlyTextField(root, ++gridRow, - Res.get("dao.factsAndFigures.dashboard.avgUSDPrice90")).second; + Res.get("dao.factsAndFigures.dashboard.avgUSDPrice90"), -20).second; avgUSDPrice30TextField = addTopLabelTextFieldWithIcon(root, gridRow, 1, - Res.get("dao.factsAndFigures.dashboard.avgUSDPrice30"), -15).second; + Res.get("dao.factsAndFigures.dashboard.avgUSDPrice30"), -35).second; AnchorPane.setRightAnchor(avgUSDPrice30TextField.getIconLabel(), 10d); avgPrice90TextField = addTopLabelReadOnlyTextField(root, ++gridRow, @@ -186,8 +186,9 @@ private void createTextFields() { } private void createPriceChart() { - addTitledGroupBg(root, ++gridRow, 2, + TitledGroupBg titledGroupBg = addTitledGroupBg(root, ++gridRow, 2, Res.get("dao.factsAndFigures.supply.priceChat"), Layout.FLOATING_LABEL_DISTANCE); + titledGroupBg.getStyleClass().add("last"); priceChartView.initialize(); VBox chartContainer = priceChartView.getRoot(); @@ -195,7 +196,7 @@ private void createPriceChart() { AnchorPane chartPane = new AnchorPane(); chartPane.getStyleClass().add("chart-pane"); AnchorPane.setTopAnchor(chartContainer, 15d); - AnchorPane.setBottomAnchor(chartContainer, 10d); + AnchorPane.setBottomAnchor(chartContainer, 0d); AnchorPane.setLeftAnchor(chartContainer, 25d); AnchorPane.setRightAnchor(chartContainer, 10d); GridPane.setColumnSpan(chartPane, 2); @@ -217,7 +218,7 @@ private void createTradeChart() { AnchorPane chartPane = new AnchorPane(); chartPane.getStyleClass().add("chart-pane"); AnchorPane.setTopAnchor(chartContainer, 15d); - AnchorPane.setBottomAnchor(chartContainer, 10d); + AnchorPane.setBottomAnchor(chartContainer, 0d); AnchorPane.setLeftAnchor(chartContainer, 25d); AnchorPane.setRightAnchor(chartContainer, 10d); GridPane.setColumnSpan(chartPane, 2); diff --git a/desktop/src/main/java/bisq/desktop/main/dao/economy/supply/SupplyView.java b/desktop/src/main/java/bisq/desktop/main/dao/economy/supply/SupplyView.java index c48316823ec..7c8816942b9 100644 --- a/desktop/src/main/java/bisq/desktop/main/dao/economy/supply/SupplyView.java +++ b/desktop/src/main/java/bisq/desktop/main/dao/economy/supply/SupplyView.java @@ -137,7 +137,7 @@ private void createDaoChart() { AnchorPane chartPane = new AnchorPane(); chartPane.getStyleClass().add("chart-pane"); AnchorPane.setTopAnchor(chartContainer, 15d); - AnchorPane.setBottomAnchor(chartContainer, 10d); + AnchorPane.setBottomAnchor(chartContainer, 0d); AnchorPane.setLeftAnchor(chartContainer, 25d); AnchorPane.setRightAnchor(chartContainer, 10d); GridPane.setColumnSpan(chartPane, 2); From 55a4154e740eb88420199964ca65a314ed067669 Mon Sep 17 00:00:00 2001 From: chimp1984 Date: Tue, 9 Feb 2021 23:28:03 -0500 Subject: [PATCH 17/21] Add tooltip to time navigation so from and to date is visible --- .../desktop/components/chart/ChartView.java | 147 +++++++++++++----- .../components/chart/ChartViewModel.java | 50 +++--- 2 files changed, 141 insertions(+), 56 deletions(-) diff --git a/desktop/src/main/java/bisq/desktop/components/chart/ChartView.java b/desktop/src/main/java/bisq/desktop/components/chart/ChartView.java index cb63032929a..24cdd2dcc2c 100644 --- a/desktop/src/main/java/bisq/desktop/components/chart/ChartView.java +++ b/desktop/src/main/java/bisq/desktop/components/chart/ChartView.java @@ -25,6 +25,8 @@ import bisq.common.UserThread; +import javafx.stage.PopupWindow; + import javafx.scene.Node; import javafx.scene.chart.Axis; import javafx.scene.chart.LineChart; @@ -44,15 +46,21 @@ import javafx.scene.layout.VBox; import javafx.scene.text.Text; +import javafx.geometry.Bounds; import javafx.geometry.Insets; import javafx.geometry.Pos; import javafx.geometry.Side; +import javafx.beans.property.SimpleStringProperty; +import javafx.beans.property.StringProperty; import javafx.beans.value.ChangeListener; +import javafx.event.EventHandler; + import javafx.collections.ListChangeListener; import javafx.collections.ObservableList; +import javafx.util.Duration; import javafx.util.StringConverter; import java.time.temporal.TemporalAdjuster; @@ -76,31 +84,31 @@ public abstract class ChartView> extends ActivatableViewAndModel { private Pane center; private SplitPane timelineNavigation; - protected NumberAxis xAxis; - protected NumberAxis yAxis; + protected NumberAxis xAxis, yAxis; protected LineChart chart; - private HBox timelineLabels; + private HBox timelineLabels, legendBox2; private final ToggleGroup timeIntervalToggleGroup = new ToggleGroup(); protected final Set> activeSeries = new HashSet<>(); protected final Map seriesIndexMap = new HashMap<>(); protected final Map legendToggleBySeriesName = new HashMap<>(); - private final List dividerNodes = new ArrayList<>(); - + private final List dividerNodesTooltips = new ArrayList<>(); private ChangeListener widthListener; private ChangeListener timeIntervalChangeListener; private ListChangeListener nodeListChangeListener; private int maxSeriesSize; - private boolean pressed; + private boolean centerPanePressed; private double x; @Setter protected boolean isRadioButtonBehaviour; @Setter private int maxDataPointsForShowingSymbols = 100; - private HBox legendBox2; private ChangeListener yAxisWidthListener; + private EventHandler dividerMouseDraggedEventHandler; + private StringProperty fromProperty = new SimpleStringProperty(); + private StringProperty toProperty = new SimpleStringProperty(); /////////////////////////////////////////////////////////////////////////////////////////// @@ -123,7 +131,7 @@ public void initialize() { prepareInitialize(); maxSeriesSize = 0; - pressed = false; + centerPanePressed = false; x = 0; // Series @@ -152,6 +160,7 @@ public void initialize() { defineAndAddActiveSeries(); // Put all together + VBox timelineNavigationBox = new VBox(); double paddingLeft = 15; double paddingRight = 89; // Y-axis width depends on data so we register a listener to get correct value @@ -172,15 +181,17 @@ public void initialize() { } } }; + VBox.setMargin(timeIntervalBox, new Insets(0, paddingRight, 0, paddingLeft)); VBox.setMargin(timelineNavigation, new Insets(0, paddingRight, 0, paddingLeft)); VBox.setMargin(timelineLabels, new Insets(0, paddingRight, 0, paddingLeft)); VBox.setMargin(legendBox1, new Insets(0, paddingRight, 0, paddingLeft)); - root.getChildren().addAll(timeIntervalBox, chart, timelineNavigation, timelineLabels, legendBox1); + timelineNavigationBox.getChildren().addAll(timelineNavigation, timelineLabels, legendBox1); if (legendBox2 != null) { VBox.setMargin(legendBox2, new Insets(-20, paddingRight, 0, paddingLeft)); - root.getChildren().add(legendBox2); + timelineNavigationBox.getChildren().add(legendBox2); } + root.getChildren().addAll(timeIntervalBox, chart, timelineNavigationBox); // Listeners widthListener = (observable, oldValue, newValue) -> { @@ -208,6 +219,8 @@ public void initialize() { @Override public void activate() { timelineNavigation.setDividerPositions(model.getDividerPositions()[0], model.getDividerPositions()[1]); + UserThread.execute(this::applyTimeLineNavigationLabels); + UserThread.execute(this::onTimelineChanged); TemporalAdjuster temporalAdjuster = model.getTemporalAdjuster(); applyTemporalAdjuster(temporalAdjuster); @@ -218,8 +231,6 @@ public void activate() { initBoundsForTimelineNavigation(); updateChartAfterDataChange(); - // Need delay to next render frame - UserThread.execute(this::applyTimeLineNavigationLabels); // Apply listeners and handlers root.widthProperty().addListener(widthListener); @@ -257,6 +268,8 @@ public void deactivate() { activeSeries.clear(); chart.getData().clear(); legendToggleBySeriesName.values().forEach(e -> e.setSelected(false)); + dividerNodes.clear(); + dividerNodesTooltips.clear(); model.invalidateCache(); } @@ -329,7 +342,6 @@ protected void onSetYAxisFormatter(XYChart.Series series) { protected LineChart getChart() { LineChart chart = new LineChart<>(xAxis, yAxis); chart.setAnimated(false); - chart.setCreateSymbols(true); chart.setLegendVisible(false); chart.setMinHeight(200); chart.setId("charts-dao"); @@ -385,9 +397,9 @@ private void addTimelineNavigation() { center = new Pane(); center.setId("chart-navigation-center-pane"); Pane right = new Pane(); - timelineNavigation = new SplitPane(); + timelineNavigation = new SplitPane(left, center, right); + timelineNavigation.setDividerPositions(model.getDividerPositions()[0], model.getDividerPositions()[1]); timelineNavigation.setMinHeight(25); - timelineNavigation.getItems().addAll(left, center, right); timelineLabels = new HBox(); } @@ -419,14 +431,26 @@ protected void applyTimeLineNavigationLabels() { } } - private void resetTimeNavigation() { - timelineNavigation.setDividerPositions(0d, 1d); - model.onTimelineNavigationChanged(0, 1); + private void onMousePressedSplitPane(MouseEvent e) { + x = e.getX(); + applyFromToDates(); + showDividerTooltips(); + } + + private void onMousePressedCenter(MouseEvent e) { + centerPanePressed = true; + applyFromToDates(); + showDividerTooltips(); } + private void onMouseReleasedCenter(MouseEvent e) { + centerPanePressed = false; + onTimelineChanged(); + hideDividerTooltips(); + } private void onMouseDragged(MouseEvent e) { - if (pressed) { + if (centerPanePressed) { double newX = e.getX(); double width = timelineNavigation.getWidth(); double relativeDelta = (x - newX) / width; @@ -437,20 +461,10 @@ private void onMouseDragged(MouseEvent e) { model.onTimelineMouseDrag(leftPos, rightPos); timelineNavigation.setDividerPositions(model.getDividerPositions()[0], model.getDividerPositions()[1]); x = newX; - } - } - private void onMouseReleasedCenter(MouseEvent e) { - pressed = false; - onTimelineChanged(); - } - - private void onMousePressedSplitPane(MouseEvent e) { - x = e.getX(); - } - - private void onMousePressedCenter(MouseEvent e) { - pressed = true; + applyFromToDates(); + showDividerTooltips(); + } } private void addActionHandlersToDividers() { @@ -459,14 +473,59 @@ private void addActionHandlersToDividers() { // and set action handler in activate. timelineNavigation.requestLayout(); timelineNavigation.applyCss(); + dividerMouseDraggedEventHandler = event -> { + applyFromToDates(); + showDividerTooltips(); + }; + for (Node node : timelineNavigation.lookupAll(".split-pane-divider")) { dividerNodes.add(node); - node.setOnMouseReleased(e -> onTimelineChanged()); + node.setOnMouseReleased(e -> { + hideDividerTooltips(); + onTimelineChanged(); + }); + node.addEventHandler(MouseEvent.MOUSE_DRAGGED, dividerMouseDraggedEventHandler); + + Tooltip tooltip = new Tooltip(""); + dividerNodesTooltips.add(tooltip); + tooltip.setShowDelay(Duration.millis(300)); + tooltip.setShowDuration(Duration.seconds(3)); + tooltip.textProperty().bind(dividerNodes.size() == 1 ? fromProperty : toProperty); + Tooltip.install(node, tooltip); } } private void removeActionHandlersToDividers() { - dividerNodes.forEach(node -> node.setOnMouseReleased(null)); + dividerNodes.forEach(node -> { + node.setOnMouseReleased(null); + node.removeEventHandler(MouseEvent.MOUSE_DRAGGED, dividerMouseDraggedEventHandler); + }); + for (int i = 0; i < dividerNodesTooltips.size(); i++) { + Tooltip tooltip = dividerNodesTooltips.get(i); + tooltip.textProperty().unbind(); + Tooltip.uninstall(dividerNodes.get(i), tooltip); + } + } + + private void resetTimeNavigation() { + timelineNavigation.setDividerPositions(0d, 1d); + model.onTimelineNavigationChanged(0, 1); + } + + private void showDividerTooltips() { + showDividerTooltip(0); + showDividerTooltip(1); + } + + private void hideDividerTooltips() { + dividerNodesTooltips.forEach(PopupWindow::hide); + } + + private void showDividerTooltip(int index) { + Node divider = dividerNodes.get(index); + Bounds bounds = divider.localToScene(divider.getBoundsInLocal()); + Tooltip tooltip = dividerNodesTooltips.get(index); + tooltip.show(divider, bounds.getMaxX(), bounds.getMaxY() - 20); } @@ -532,15 +591,29 @@ private void onTimeIntervalChanged(Toggle newValue) { } private void onTimelineChanged() { + updateTimeLinePositions(); + + model.invalidateCache(); + applyData(); + updateChartAfterDataChange(); + } + + private void updateTimeLinePositions() { double leftPos = timelineNavigation.getDividerPositions()[0]; double rightPos = timelineNavigation.getDividerPositions()[1]; model.onTimelineNavigationChanged(leftPos, rightPos); // We need to update as model might have adjusted the values timelineNavigation.setDividerPositions(model.getDividerPositions()[0], model.getDividerPositions()[1]); + fromProperty.set(model.getTimeAxisStringConverter().toString(model.getFromDate()).replace("\n", " ")); + toProperty.set(model.getTimeAxisStringConverter().toString(model.getToDate()).replace("\n", " ")); + } - model.invalidateCache(); - applyData(); - updateChartAfterDataChange(); + private void applyFromToDates() { + double leftPos = timelineNavigation.getDividerPositions()[0]; + double rightPos = timelineNavigation.getDividerPositions()[1]; + model.applyFromToDates(leftPos, rightPos); + fromProperty.set(model.getTimeAxisStringConverter().toString(model.getFromDate()).replace("\n", " ")); + toProperty.set(model.getTimeAxisStringConverter().toString(model.getToDate()).replace("\n", " ")); } private void onSelectLegendToggle(XYChart.Series series) { diff --git a/desktop/src/main/java/bisq/desktop/components/chart/ChartViewModel.java b/desktop/src/main/java/bisq/desktop/components/chart/ChartViewModel.java index a4cdd60debe..25a7de5b2fa 100644 --- a/desktop/src/main/java/bisq/desktop/components/chart/ChartViewModel.java +++ b/desktop/src/main/java/bisq/desktop/components/chart/ChartViewModel.java @@ -50,6 +50,10 @@ public abstract class ChartViewModel extends Activatab protected Number upperBound; @Getter protected String dateFormatPatters = "dd MMM\nyyyy"; + @Getter + long fromDate; + @Getter + long toDate; public ChartViewModel(T dataModel) { super(dataModel); @@ -94,23 +98,7 @@ protected TemporalAdjuster getTemporalAdjuster() { /////////////////////////////////////////////////////////////////////////////////////////// void onTimelineNavigationChanged(double leftPos, double rightPos) { - long from, to; - - // We need to snap into the 0 and 1 values once we are close as otherwise once navigation has been used we - // would not get back to exact 0 or 1. Not clear why but might be rounding issues from values at x positions of - // drag operations. - if (leftPos < LEFT_TIMELINE_SNAP_VALUE) { - leftPos = 0; - } - if (rightPos > RIGHT_TIMELINE_SNAP_VALUE) { - rightPos = 1; - } - dividerPositions[0] = leftPos; - dividerPositions[1] = rightPos; - - long lowerBoundAsLong = lowerBound.longValue(); - long totalRange = upperBound.longValue() - lowerBoundAsLong; - + applyFromToDates(leftPos, rightPos); // TODO find better solution // The TemporalAdjusters map dates to the lower bound (e.g. 1.1.2016) but our from date is the date of // the first data entry so if we filter by that we would exclude the first year data in case YEAR was selected @@ -119,20 +107,44 @@ void onTimelineNavigationChanged(double leftPos, double rightPos) { // the case when we have not set the date filter (left =0 / right =1) we set from date to epoch time 0 and // to date to one year ahead to be sure we include all. + long from, to; + + // We only manipulate the from, to variables for the date filter, not the fromDate, toDate properties as those + // are used by the view for tooltip over the time line navigation dividers if (leftPos == 0) { from = 0; } else { - from = (long) (lowerBoundAsLong + totalRange * leftPos); + from = fromDate; } if (rightPos == 1) { to = new Date().getTime() / 1000 + TimeUnit.DAYS.toSeconds(365); } else { - to = (long) (lowerBoundAsLong + totalRange * rightPos); + to = toDate; } + dividerPositions[0] = leftPos; + dividerPositions[1] = rightPos; dataModel.setDateFilter(from, to); } + void applyFromToDates(double leftPos, double rightPos) { + // We need to snap into the 0 and 1 values once we are close as otherwise once navigation has been used we + // would not get back to exact 0 or 1. Not clear why but might be rounding issues from values at x positions of + // drag operations. + if (leftPos < LEFT_TIMELINE_SNAP_VALUE) { + leftPos = 0; + } + if (rightPos > RIGHT_TIMELINE_SNAP_VALUE) { + rightPos = 1; + } + + long lowerBoundAsLong = lowerBound.longValue(); + long totalRange = upperBound.longValue() - lowerBoundAsLong; + + fromDate = (long) (lowerBoundAsLong + totalRange * leftPos); + toDate = (long) (lowerBoundAsLong + totalRange * rightPos); + } + void onTimelineMouseDrag(double leftPos, double rightPos) { // Limit drag operation if we have hit a boundary if (leftPos > LEFT_TIMELINE_SNAP_VALUE) { From e91ed8a857dcaa8d97d8b5161157fd9ebf3e3fff Mon Sep 17 00:00:00 2001 From: chimp1984 Date: Wed, 10 Feb 2021 01:14:57 -0500 Subject: [PATCH 18/21] Always update all text field values even if the series is not selected --- .../bisq/desktop/components/chart/ChartView.java | 8 ++++---- .../main/dao/economy/supply/dao/DaoChartView.java | 13 +++++-------- 2 files changed, 9 insertions(+), 12 deletions(-) diff --git a/desktop/src/main/java/bisq/desktop/components/chart/ChartView.java b/desktop/src/main/java/bisq/desktop/components/chart/ChartView.java index 24cdd2dcc2c..57b3acc5a6c 100644 --- a/desktop/src/main/java/bisq/desktop/components/chart/ChartView.java +++ b/desktop/src/main/java/bisq/desktop/components/chart/ChartView.java @@ -61,7 +61,6 @@ import javafx.collections.ObservableList; import javafx.util.Duration; -import javafx.util.StringConverter; import java.time.temporal.TemporalAdjuster; @@ -322,6 +321,7 @@ protected void applyTemporalAdjuster(TemporalAdjuster temporalAdjuster) { protected NumberAxis getXAxis() { NumberAxis xAxis = new NumberAxis(); xAxis.setForceZeroInRange(false); + xAxis.setAutoRanging(true); xAxis.setTickLabelFormatter(model.getTimeAxisStringConverter()); return xAxis; } @@ -330,8 +330,7 @@ protected NumberAxis getYAxis() { NumberAxis yAxis = new NumberAxis(); yAxis.setForceZeroInRange(true); yAxis.setSide(Side.RIGHT); - StringConverter yAxisStringConverter = model.getYAxisStringConverter(); - yAxis.setTickLabelFormatter(yAxisStringConverter); + yAxis.setTickLabelFormatter(model.getYAxisStringConverter()); return yAxis; } @@ -525,7 +524,8 @@ private void showDividerTooltip(int index) { Node divider = dividerNodes.get(index); Bounds bounds = divider.localToScene(divider.getBoundsInLocal()); Tooltip tooltip = dividerNodesTooltips.get(index); - tooltip.show(divider, bounds.getMaxX(), bounds.getMaxY() - 20); + double xOffset = index == 0 ? 75 : 0; + tooltip.show(divider, bounds.getMaxX() - xOffset, bounds.getMaxY() - 20); } diff --git a/desktop/src/main/java/bisq/desktop/main/dao/economy/supply/dao/DaoChartView.java b/desktop/src/main/java/bisq/desktop/main/dao/economy/supply/dao/DaoChartView.java index 9e0e3dfbab6..637ea91b01d 100644 --- a/desktop/src/main/java/bisq/desktop/main/dao/economy/supply/dao/DaoChartView.java +++ b/desktop/src/main/java/bisq/desktop/main/dao/economy/supply/dao/DaoChartView.java @@ -194,37 +194,34 @@ protected void applyData() { if (activeSeries.contains(seriesProofOfBurn)) { applyProofOfBurn(); } + + compensationAmountProperty.set(model.getCompensationAmount()); + reimbursementAmountProperty.set(model.getReimbursementAmount()); + bsqTradeFeeAmountProperty.set(model.getBsqTradeFeeAmount()); + proofOfBurnAmountProperty.set(model.getProofOfBurnAmount()); } private void applyTotalIssued() { seriesTotalIssued.getData().setAll(model.getTotalIssuedChartData()); - compensationAmountProperty.set(model.getCompensationAmount()); - reimbursementAmountProperty.set(model.getReimbursementAmount()); } private void applyCompensation() { seriesCompensation.getData().setAll(model.getCompensationChartData()); - compensationAmountProperty.set(model.getCompensationAmount()); } private void applyReimbursement() { seriesReimbursement.getData().setAll(model.getReimbursementChartData()); - reimbursementAmountProperty.set(model.getReimbursementAmount()); } private void applyTotalBurned() { seriesTotalBurned.getData().setAll(model.getTotalBurnedChartData()); - bsqTradeFeeAmountProperty.set(model.getBsqTradeFeeAmount()); - proofOfBurnAmountProperty.set(model.getProofOfBurnAmount()); } private void applyBsqTradeFee() { seriesBsqTradeFee.getData().setAll(model.getBsqTradeFeeChartData()); - bsqTradeFeeAmountProperty.set(model.getBsqTradeFeeAmount()); } private void applyProofOfBurn() { seriesProofOfBurn.getData().setAll(model.getProofOfBurnChartData()); - proofOfBurnAmountProperty.set(model.getProofOfBurnAmount()); } } From 683e768a00db0172f4b557c55267371ba47577b9 Mon Sep 17 00:00:00 2001 From: chimp1984 Date: Wed, 10 Feb 2021 01:41:45 -0500 Subject: [PATCH 19/21] Add total volume text fields --- .../resources/i18n/displayStrings.properties | 2 ++ .../economy/dashboard/BsqDashboardView.java | 33 ++++++++++++++++++- .../volume/VolumeChartDataModel.java | 17 ++++++++++ .../dashboard/volume/VolumeChartView.java | 24 ++++++++++++++ .../volume/VolumeChartViewModel.java | 13 ++++++++ .../dao/economy/supply/dao/DaoChartView.java | 4 --- 6 files changed, 88 insertions(+), 5 deletions(-) diff --git a/core/src/main/resources/i18n/displayStrings.properties b/core/src/main/resources/i18n/displayStrings.properties index 7889f156072..347b3785ad1 100644 --- a/core/src/main/resources/i18n/displayStrings.properties +++ b/core/src/main/resources/i18n/displayStrings.properties @@ -2434,6 +2434,8 @@ dao.factsAndFigures.dashboard.avgUSDPrice90=90 days volume weighted average BSQ/ dao.factsAndFigures.dashboard.avgUSDPrice30=30 days volume weighted average BSQ/USD price dao.factsAndFigures.dashboard.marketCap=Market capitalisation (based on 30 days average BSQ/USD price) dao.factsAndFigures.dashboard.availableAmount=Total available BSQ +dao.factsAndFigures.dashboard.volumeUsd=Total trade volume in USD +dao.factsAndFigures.dashboard.volumeBtc=Total trade volume in BTC dao.factsAndFigures.supply.issuedVsBurnt=BSQ issued v. BSQ burnt diff --git a/desktop/src/main/java/bisq/desktop/main/dao/economy/dashboard/BsqDashboardView.java b/desktop/src/main/java/bisq/desktop/main/dao/economy/dashboard/BsqDashboardView.java index 8ac6b49f7a8..dabc66c129f 100644 --- a/desktop/src/main/java/bisq/desktop/main/dao/economy/dashboard/BsqDashboardView.java +++ b/desktop/src/main/java/bisq/desktop/main/dao/economy/dashboard/BsqDashboardView.java @@ -29,6 +29,7 @@ import bisq.core.dao.state.DaoStateListener; import bisq.core.dao.state.model.blockchain.Block; import bisq.core.dao.state.model.governance.IssuanceType; +import bisq.core.locale.GlobalSettings; import bisq.core.locale.Res; import bisq.core.monetary.Price; import bisq.core.provider.price.PriceFeedService; @@ -38,6 +39,7 @@ import bisq.core.util.FormattingUtils; import bisq.core.util.coin.BsqFormatter; +import bisq.common.util.MathUtils; import bisq.common.util.Tuple2; import bisq.common.util.Tuple3; @@ -55,10 +57,13 @@ import javafx.geometry.Insets; +import javafx.beans.binding.Bindings; import javafx.beans.value.ChangeListener; import javafx.collections.ObservableList; +import java.text.DecimalFormat; + import java.util.Optional; import static bisq.desktop.util.FormBuilder.addLabelWithSubText; @@ -78,7 +83,7 @@ public class BsqDashboardView extends ActivatableView implements private final Preferences preferences; private final BsqFormatter bsqFormatter; - private TextField avgPrice90TextField, avgUSDPrice90TextField, marketCapTextField, availableAmountTextField; + private TextField avgPrice90TextField, avgUSDPrice90TextField, marketCapTextField, availableAmountTextField, usdVolumeTextField, btcVolumeTextField; private TextFieldWithIcon avgPrice30TextField, avgUSDPrice30TextField; private Label marketPriceLabel; @@ -133,6 +138,24 @@ protected void activate() { updateAveragePriceFields(avgPrice90TextField, avgPrice30TextField, false); updateAveragePriceFields(avgUSDPrice90TextField, avgUSDPrice30TextField, true); updateMarketCap(); + + usdVolumeTextField.textProperty().bind(Bindings.createStringBinding( + () -> { + DecimalFormat volumeFormat = (DecimalFormat) DecimalFormat.getNumberInstance(GlobalSettings.getLocale()); + volumeFormat.setMaximumFractionDigits(0); + double scaled = MathUtils.scaleDownByPowerOf10(volumeChartView.usdVolumeProperty().get(), 4); + return volumeFormat.format(scaled) + " USD"; + }, + volumeChartView.usdVolumeProperty())); + + btcVolumeTextField.textProperty().bind(Bindings.createStringBinding( + () -> { + DecimalFormat volumeFormat = (DecimalFormat) DecimalFormat.getNumberInstance(GlobalSettings.getLocale()); + volumeFormat.setMaximumFractionDigits(4); + double scaled = MathUtils.scaleDownByPowerOf10(volumeChartView.btcVolumeProperty().get(), 8); + return volumeFormat.format(scaled) + " BTC"; + }, + volumeChartView.btcVolumeProperty())); } @@ -140,6 +163,9 @@ protected void activate() { protected void deactivate() { daoFacade.removeBsqStateListener(this); priceFeedService.updateCounterProperty().removeListener(priceChangeListener); + + usdVolumeTextField.textProperty().unbind(); + btcVolumeTextField.textProperty().unbind(); } @@ -227,6 +253,11 @@ private void createTradeChart() { chartPane.getChildren().add(chartContainer); root.getChildren().add(chartPane); + + usdVolumeTextField = addTopLabelReadOnlyTextField(root, ++gridRow, + Res.get("dao.factsAndFigures.dashboard.volumeUsd")).second; + btcVolumeTextField = addTopLabelReadOnlyTextField(root, gridRow, 1, + Res.get("dao.factsAndFigures.dashboard.volumeBtc")).second; } private void updateWithBsqBlockChainData() { diff --git a/desktop/src/main/java/bisq/desktop/main/dao/economy/dashboard/volume/VolumeChartDataModel.java b/desktop/src/main/java/bisq/desktop/main/dao/economy/dashboard/volume/VolumeChartDataModel.java index 4dc7feb7272..e147ce69772 100644 --- a/desktop/src/main/java/bisq/desktop/main/dao/economy/dashboard/volume/VolumeChartDataModel.java +++ b/desktop/src/main/java/bisq/desktop/main/dao/economy/dashboard/volume/VolumeChartDataModel.java @@ -54,6 +54,23 @@ public VolumeChartDataModel(TradeStatisticsManager tradeStatisticsManager) { } + /////////////////////////////////////////////////////////////////////////////////////////// + // Total amounts + /////////////////////////////////////////////////////////////////////////////////////////// + + long getUsdVolume() { + return getUsdVolumeByInterval().values().stream() + .mapToLong(e -> e) + .sum(); + } + + long getBtcVolume() { + return getBtcVolumeByInterval().values().stream() + .mapToLong(e -> e) + .sum(); + } + + /////////////////////////////////////////////////////////////////////////////////////////// // Data /////////////////////////////////////////////////////////////////////////////////////////// diff --git a/desktop/src/main/java/bisq/desktop/main/dao/economy/dashboard/volume/VolumeChartView.java b/desktop/src/main/java/bisq/desktop/main/dao/economy/dashboard/volume/VolumeChartView.java index a18a3156347..eb4c6995c3f 100644 --- a/desktop/src/main/java/bisq/desktop/main/dao/economy/dashboard/volume/VolumeChartView.java +++ b/desktop/src/main/java/bisq/desktop/main/dao/economy/dashboard/volume/VolumeChartView.java @@ -25,6 +25,10 @@ import javafx.scene.chart.XYChart; +import javafx.beans.property.LongProperty; +import javafx.beans.property.ReadOnlyLongProperty; +import javafx.beans.property.SimpleLongProperty; + import java.util.Collection; import java.util.List; @@ -34,6 +38,9 @@ public class VolumeChartView extends ChartView { private XYChart.Series seriesUsdVolume, seriesBtcVolume; + private LongProperty usdVolumeProperty = new SimpleLongProperty(); + private LongProperty btcVolumeProperty = new SimpleLongProperty(); + @Inject public VolumeChartView(VolumeChartViewModel model) { super(model); @@ -121,5 +128,22 @@ protected void applyData() { if (activeSeries.contains(seriesBtcVolume)) { seriesBtcVolume.getData().setAll(model.getBtcVolumeChartData()); } + + usdVolumeProperty.set(model.getUsdVolume()); + btcVolumeProperty.set(model.getBtcVolume()); + } + + public ReadOnlyLongProperty usdVolumeProperty() { + if (usdVolumeProperty.get() == 0) { + usdVolumeProperty.set(model.getUsdVolume()); + } + return usdVolumeProperty; + } + + public ReadOnlyLongProperty btcVolumeProperty() { + if (btcVolumeProperty.get() == 0) { + btcVolumeProperty.set(model.getBtcVolume()); + } + return btcVolumeProperty; } } diff --git a/desktop/src/main/java/bisq/desktop/main/dao/economy/dashboard/volume/VolumeChartViewModel.java b/desktop/src/main/java/bisq/desktop/main/dao/economy/dashboard/volume/VolumeChartViewModel.java index 5d914ab6c25..142d2f3912d 100644 --- a/desktop/src/main/java/bisq/desktop/main/dao/economy/dashboard/volume/VolumeChartViewModel.java +++ b/desktop/src/main/java/bisq/desktop/main/dao/economy/dashboard/volume/VolumeChartViewModel.java @@ -50,6 +50,19 @@ public VolumeChartViewModel(VolumeChartDataModel dataModel) { } + /////////////////////////////////////////////////////////////////////////////////////////// + // Total amounts + /////////////////////////////////////////////////////////////////////////////////////////// + + long getUsdVolume() { + return dataModel.getUsdVolume(); + } + + long getBtcVolume() { + return dataModel.getBtcVolume(); + } + + /////////////////////////////////////////////////////////////////////////////////////////// // Chart data /////////////////////////////////////////////////////////////////////////////////////////// diff --git a/desktop/src/main/java/bisq/desktop/main/dao/economy/supply/dao/DaoChartView.java b/desktop/src/main/java/bisq/desktop/main/dao/economy/supply/dao/DaoChartView.java index 637ea91b01d..cccd23fc35e 100644 --- a/desktop/src/main/java/bisq/desktop/main/dao/economy/supply/dao/DaoChartView.java +++ b/desktop/src/main/java/bisq/desktop/main/dao/economy/supply/dao/DaoChartView.java @@ -55,10 +55,6 @@ public DaoChartView(DaoChartViewModel model) { // API Total amounts /////////////////////////////////////////////////////////////////////////////////////////// - public long getCompensationAmountProperty() { - return model.getCompensationAmount(); - } - public ReadOnlyLongProperty compensationAmountProperty() { if (compensationAmountProperty.get() == 0) { compensationAmountProperty.set(model.getCompensationAmount()); From d04ac5cdf0cfbff7554ec80b7c214c2576bf79b2 Mon Sep 17 00:00:00 2001 From: chimp1984 Date: Wed, 10 Feb 2021 20:50:12 -0500 Subject: [PATCH 20/21] Add missing stage offset to tooltips --- .../main/java/bisq/desktop/components/chart/ChartView.java | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/desktop/src/main/java/bisq/desktop/components/chart/ChartView.java b/desktop/src/main/java/bisq/desktop/components/chart/ChartView.java index 57b3acc5a6c..da278463d0e 100644 --- a/desktop/src/main/java/bisq/desktop/components/chart/ChartView.java +++ b/desktop/src/main/java/bisq/desktop/components/chart/ChartView.java @@ -26,6 +26,7 @@ import bisq.common.UserThread; import javafx.stage.PopupWindow; +import javafx.stage.Stage; import javafx.scene.Node; import javafx.scene.chart.Axis; @@ -524,8 +525,10 @@ private void showDividerTooltip(int index) { Node divider = dividerNodes.get(index); Bounds bounds = divider.localToScene(divider.getBoundsInLocal()); Tooltip tooltip = dividerNodesTooltips.get(index); - double xOffset = index == 0 ? 75 : 0; - tooltip.show(divider, bounds.getMaxX() - xOffset, bounds.getMaxY() - 20); + double xOffset = index == 0 ? -90 : 10; + Stage stage = (Stage) root.getScene().getWindow(); + tooltip.show(stage, stage.getX() + bounds.getMaxX() + xOffset, + stage.getY() + bounds.getMaxY() - 40); } From cd0a28361f2c7b76b66f40f878af4d8a63d26d54 Mon Sep 17 00:00:00 2001 From: chimp1984 Date: Wed, 10 Feb 2021 22:38:40 -0500 Subject: [PATCH 21/21] Add average bsq price text fields from timeline selection --- .../resources/i18n/displayStrings.properties | 2 ++ .../components/chart/ChartDataModel.java | 3 +- .../components/chart/ChartViewModel.java | 4 +-- .../chart/TemporalAdjusterModel.java | 2 +- .../economy/dashboard/BsqDashboardView.java | 34 +++++++++++++++++-- .../dashboard/price/PriceChartDataModel.java | 29 ++++++++++++++++ .../dashboard/price/PriceChartView.java | 23 ++++++++++++- .../dashboard/price/PriceChartViewModel.java | 13 +++++++ .../dashboard/volume/VolumeChartView.java | 27 +++++++-------- .../dao/economy/supply/dao/DaoChartView.java | 12 ------- 10 files changed, 115 insertions(+), 34 deletions(-) diff --git a/core/src/main/resources/i18n/displayStrings.properties b/core/src/main/resources/i18n/displayStrings.properties index 347b3785ad1..edda39ae603 100644 --- a/core/src/main/resources/i18n/displayStrings.properties +++ b/core/src/main/resources/i18n/displayStrings.properties @@ -2436,6 +2436,8 @@ dao.factsAndFigures.dashboard.marketCap=Market capitalisation (based on 30 days dao.factsAndFigures.dashboard.availableAmount=Total available BSQ dao.factsAndFigures.dashboard.volumeUsd=Total trade volume in USD dao.factsAndFigures.dashboard.volumeBtc=Total trade volume in BTC +dao.factsAndFigures.dashboard.averageBsqUsdPriceFromSelection=Average BSQ/USD trade price from selected time period in chart +dao.factsAndFigures.dashboard.averageBsqBtcPriceFromSelection=Average BSQ/BTC trade price from selected time period in chart dao.factsAndFigures.supply.issuedVsBurnt=BSQ issued v. BSQ burnt diff --git a/desktop/src/main/java/bisq/desktop/components/chart/ChartDataModel.java b/desktop/src/main/java/bisq/desktop/components/chart/ChartDataModel.java index b8ab438873e..610ed63b03c 100644 --- a/desktop/src/main/java/bisq/desktop/components/chart/ChartDataModel.java +++ b/desktop/src/main/java/bisq/desktop/components/chart/ChartDataModel.java @@ -72,10 +72,11 @@ public Predicate getDateFilter() { return dateFilter; } - void setDateFilter(double from, double to) { + void setDateFilter(long from, long to) { dateFilter = value -> value >= from && value <= to; } + /////////////////////////////////////////////////////////////////////////////////////////// // Data /////////////////////////////////////////////////////////////////////////////////////////// diff --git a/desktop/src/main/java/bisq/desktop/components/chart/ChartViewModel.java b/desktop/src/main/java/bisq/desktop/components/chart/ChartViewModel.java index 25a7de5b2fa..76d7f39631f 100644 --- a/desktop/src/main/java/bisq/desktop/components/chart/ChartViewModel.java +++ b/desktop/src/main/java/bisq/desktop/components/chart/ChartViewModel.java @@ -111,12 +111,12 @@ void onTimelineNavigationChanged(double leftPos, double rightPos) { // We only manipulate the from, to variables for the date filter, not the fromDate, toDate properties as those // are used by the view for tooltip over the time line navigation dividers - if (leftPos == 0) { + if (leftPos < LEFT_TIMELINE_SNAP_VALUE) { from = 0; } else { from = fromDate; } - if (rightPos == 1) { + if (rightPos > RIGHT_TIMELINE_SNAP_VALUE) { to = new Date().getTime() / 1000 + TimeUnit.DAYS.toSeconds(365); } else { to = toDate; diff --git a/desktop/src/main/java/bisq/desktop/components/chart/TemporalAdjusterModel.java b/desktop/src/main/java/bisq/desktop/components/chart/TemporalAdjusterModel.java index cd029826fe1..b304cfc7e45 100644 --- a/desktop/src/main/java/bisq/desktop/components/chart/TemporalAdjusterModel.java +++ b/desktop/src/main/java/bisq/desktop/components/chart/TemporalAdjusterModel.java @@ -44,7 +44,7 @@ public enum Interval { } } - protected TemporalAdjuster temporalAdjuster = Interval.MONTH.getAdjuster(); + protected TemporalAdjuster temporalAdjuster = Interval.DAY.getAdjuster(); public void setTemporalAdjuster(TemporalAdjuster temporalAdjuster) { this.temporalAdjuster = temporalAdjuster; diff --git a/desktop/src/main/java/bisq/desktop/main/dao/economy/dashboard/BsqDashboardView.java b/desktop/src/main/java/bisq/desktop/main/dao/economy/dashboard/BsqDashboardView.java index dabc66c129f..b4d4c9dd897 100644 --- a/desktop/src/main/java/bisq/desktop/main/dao/economy/dashboard/BsqDashboardView.java +++ b/desktop/src/main/java/bisq/desktop/main/dao/economy/dashboard/BsqDashboardView.java @@ -83,7 +83,8 @@ public class BsqDashboardView extends ActivatableView implements private final Preferences preferences; private final BsqFormatter bsqFormatter; - private TextField avgPrice90TextField, avgUSDPrice90TextField, marketCapTextField, availableAmountTextField, usdVolumeTextField, btcVolumeTextField; + private TextField avgPrice90TextField, avgUSDPrice90TextField, marketCapTextField, availableAmountTextField, + usdVolumeTextField, btcVolumeTextField, averageBsqUsdPriceTextField, averageBsqBtcPriceTextField; private TextFieldWithIcon avgPrice30TextField, avgUSDPrice30TextField; private Label marketPriceLabel; @@ -139,6 +140,27 @@ protected void activate() { updateAveragePriceFields(avgUSDPrice90TextField, avgUSDPrice30TextField, true); updateMarketCap(); + averageBsqUsdPriceTextField.textProperty().bind(Bindings.createStringBinding( + () -> { + DecimalFormat priceFormat = (DecimalFormat) DecimalFormat.getNumberInstance(GlobalSettings.getLocale()); + priceFormat.setMaximumFractionDigits(4); + return priceFormat.format(priceChartView.averageBsqUsdPriceProperty().get()) + " BSQ/USD"; + }, + priceChartView.averageBsqUsdPriceProperty())); + averageBsqBtcPriceTextField.textProperty().bind(Bindings.createStringBinding( + () -> { + DecimalFormat priceFormat = (DecimalFormat) DecimalFormat.getNumberInstance(GlobalSettings.getLocale()); + priceFormat.setMaximumFractionDigits(8); + /* yAxisFormatter = value -> { + value = MathUtils.scaleDownByPowerOf10(value.longValue(), 8); + return priceFormat.format(value) + " BSQ/BTC"; + };*/ + + double scaled = MathUtils.scaleDownByPowerOf10(priceChartView.averageBsqBtcPriceProperty().get(), 8); + return priceFormat.format(scaled) + " BSQ/BTC"; + }, + priceChartView.averageBsqBtcPriceProperty())); + usdVolumeTextField.textProperty().bind(Bindings.createStringBinding( () -> { DecimalFormat volumeFormat = (DecimalFormat) DecimalFormat.getNumberInstance(GlobalSettings.getLocale()); @@ -147,7 +169,6 @@ protected void activate() { return volumeFormat.format(scaled) + " USD"; }, volumeChartView.usdVolumeProperty())); - btcVolumeTextField.textProperty().bind(Bindings.createStringBinding( () -> { DecimalFormat volumeFormat = (DecimalFormat) DecimalFormat.getNumberInstance(GlobalSettings.getLocale()); @@ -164,11 +185,12 @@ protected void deactivate() { daoFacade.removeBsqStateListener(this); priceFeedService.updateCounterProperty().removeListener(priceChangeListener); + averageBsqUsdPriceTextField.textProperty().unbind(); + averageBsqBtcPriceTextField.textProperty().unbind(); usdVolumeTextField.textProperty().unbind(); btcVolumeTextField.textProperty().unbind(); } - /////////////////////////////////////////////////////////////////////////////////////////// // DaoStateListener /////////////////////////////////////////////////////////////////////////////////////////// @@ -231,6 +253,12 @@ private void createPriceChart() { chartPane.getChildren().add(chartContainer); root.getChildren().add(chartPane); + + averageBsqUsdPriceTextField = addTopLabelReadOnlyTextField(root, ++gridRow, + Res.get("dao.factsAndFigures.dashboard.averageBsqUsdPriceFromSelection")).second; + averageBsqBtcPriceTextField = addTopLabelReadOnlyTextField(root, gridRow, 1, + Res.get("dao.factsAndFigures.dashboard.averageBsqBtcPriceFromSelection")).second; + } private void createTradeChart() { diff --git a/desktop/src/main/java/bisq/desktop/main/dao/economy/dashboard/price/PriceChartDataModel.java b/desktop/src/main/java/bisq/desktop/main/dao/economy/dashboard/price/PriceChartDataModel.java index e10859e0bfb..49b2a0a7b4f 100644 --- a/desktop/src/main/java/bisq/desktop/main/dao/economy/dashboard/price/PriceChartDataModel.java +++ b/desktop/src/main/java/bisq/desktop/main/dao/economy/dashboard/price/PriceChartDataModel.java @@ -67,6 +67,27 @@ protected void invalidateCache() { btcUsdPriceByInterval = null; } + + /////////////////////////////////////////////////////////////////////////////////////////// + // Average price from timeline selection + /////////////////////////////////////////////////////////////////////////////////////////// + + double averageBsqUsdPrice() { + return getAveragePriceFromDateFilter(tradeStatistics -> tradeStatistics.getCurrency().equals("BSQ") || + tradeStatistics.getCurrency().equals("USD"), + PriceChartDataModel::getAverageBsqUsdPrice); + } + + double averageBsqBtcPrice() { + return getAveragePriceFromDateFilter(tradeStatistics -> tradeStatistics.getCurrency().equals("BSQ"), + PriceChartDataModel::getAverageBsqBtcPrice); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Chart data + /////////////////////////////////////////////////////////////////////////////////////////// + Map getBsqUsdPriceByInterval() { if (bsqUsdPriceByInterval != null) { return bsqUsdPriceByInterval; @@ -191,4 +212,12 @@ private Map getPriceByInterval(Collection collec .filter(e -> e.getValue() > 0d) .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); } + + private double getAveragePriceFromDateFilter(Predicate collectionFilter, + Function, Double> getAveragePriceFunction) { + return getAveragePriceFunction.apply(tradeStatisticsManager.getObservableTradeStatisticsSet().stream() + .filter(collectionFilter) + .filter(tradeStatistics -> dateFilter.test(tradeStatistics.getDateAsLong() / 1000)) + .collect(Collectors.toList())); + } } diff --git a/desktop/src/main/java/bisq/desktop/main/dao/economy/dashboard/price/PriceChartView.java b/desktop/src/main/java/bisq/desktop/main/dao/economy/dashboard/price/PriceChartView.java index d9d69bdc989..0245b028a01 100644 --- a/desktop/src/main/java/bisq/desktop/main/dao/economy/dashboard/price/PriceChartView.java +++ b/desktop/src/main/java/bisq/desktop/main/dao/economy/dashboard/price/PriceChartView.java @@ -25,6 +25,10 @@ import javafx.scene.chart.XYChart; +import javafx.beans.property.DoubleProperty; +import javafx.beans.property.ReadOnlyDoubleProperty; +import javafx.beans.property.SimpleDoubleProperty; + import java.util.Collection; import java.util.List; @@ -33,7 +37,8 @@ @Slf4j public class PriceChartView extends ChartView { private XYChart.Series seriesBsqUsdPrice, seriesBsqBtcPrice, seriesBtcUsdPrice; - + private DoubleProperty averageBsqUsdPriceProperty = new SimpleDoubleProperty(); + private DoubleProperty averageBsqBtcPriceProperty = new SimpleDoubleProperty(); @Inject public PriceChartView(PriceChartViewModel model) { @@ -43,6 +48,19 @@ public PriceChartView(PriceChartViewModel model) { } + /////////////////////////////////////////////////////////////////////////////////////////// + // API + /////////////////////////////////////////////////////////////////////////////////////////// + + public ReadOnlyDoubleProperty averageBsqUsdPriceProperty() { + return averageBsqUsdPriceProperty; + } + + public ReadOnlyDoubleProperty averageBsqBtcPriceProperty() { + return averageBsqBtcPriceProperty; + } + + /////////////////////////////////////////////////////////////////////////////////////////// // Chart /////////////////////////////////////////////////////////////////////////////////////////// @@ -133,5 +151,8 @@ protected void applyData() { if (activeSeries.contains(seriesBtcUsdPrice)) { seriesBtcUsdPrice.getData().setAll(model.getBtcUsdPriceChartData()); } + + averageBsqBtcPriceProperty.set(model.averageBsqBtcPrice()); + averageBsqUsdPriceProperty.set(model.averageBsqUsdPrice()); } } diff --git a/desktop/src/main/java/bisq/desktop/main/dao/economy/dashboard/price/PriceChartViewModel.java b/desktop/src/main/java/bisq/desktop/main/dao/economy/dashboard/price/PriceChartViewModel.java index 891be72d337..6074a279d30 100644 --- a/desktop/src/main/java/bisq/desktop/main/dao/economy/dashboard/price/PriceChartViewModel.java +++ b/desktop/src/main/java/bisq/desktop/main/dao/economy/dashboard/price/PriceChartViewModel.java @@ -49,6 +49,19 @@ public PriceChartViewModel(PriceChartDataModel dataModel) { } + /////////////////////////////////////////////////////////////////////////////////////////// + // Average price from timeline selection + /////////////////////////////////////////////////////////////////////////////////////////// + + double averageBsqUsdPrice() { + return dataModel.averageBsqUsdPrice(); + } + + double averageBsqBtcPrice() { + return dataModel.averageBsqBtcPrice(); + } + + /////////////////////////////////////////////////////////////////////////////////////////// // Chart data /////////////////////////////////////////////////////////////////////////////////////////// diff --git a/desktop/src/main/java/bisq/desktop/main/dao/economy/dashboard/volume/VolumeChartView.java b/desktop/src/main/java/bisq/desktop/main/dao/economy/dashboard/volume/VolumeChartView.java index eb4c6995c3f..2dff0a73ea4 100644 --- a/desktop/src/main/java/bisq/desktop/main/dao/economy/dashboard/volume/VolumeChartView.java +++ b/desktop/src/main/java/bisq/desktop/main/dao/economy/dashboard/volume/VolumeChartView.java @@ -49,6 +49,19 @@ public VolumeChartView(VolumeChartViewModel model) { } + /////////////////////////////////////////////////////////////////////////////////////////// + // API + /////////////////////////////////////////////////////////////////////////////////////////// + + public ReadOnlyLongProperty usdVolumeProperty() { + return usdVolumeProperty; + } + + public ReadOnlyLongProperty btcVolumeProperty() { + return btcVolumeProperty; + } + + /////////////////////////////////////////////////////////////////////////////////////////// // Chart /////////////////////////////////////////////////////////////////////////////////////////// @@ -132,18 +145,4 @@ protected void applyData() { usdVolumeProperty.set(model.getUsdVolume()); btcVolumeProperty.set(model.getBtcVolume()); } - - public ReadOnlyLongProperty usdVolumeProperty() { - if (usdVolumeProperty.get() == 0) { - usdVolumeProperty.set(model.getUsdVolume()); - } - return usdVolumeProperty; - } - - public ReadOnlyLongProperty btcVolumeProperty() { - if (btcVolumeProperty.get() == 0) { - btcVolumeProperty.set(model.getBtcVolume()); - } - return btcVolumeProperty; - } } diff --git a/desktop/src/main/java/bisq/desktop/main/dao/economy/supply/dao/DaoChartView.java b/desktop/src/main/java/bisq/desktop/main/dao/economy/supply/dao/DaoChartView.java index cccd23fc35e..8cf2e7cc88a 100644 --- a/desktop/src/main/java/bisq/desktop/main/dao/economy/supply/dao/DaoChartView.java +++ b/desktop/src/main/java/bisq/desktop/main/dao/economy/supply/dao/DaoChartView.java @@ -56,30 +56,18 @@ public DaoChartView(DaoChartViewModel model) { /////////////////////////////////////////////////////////////////////////////////////////// public ReadOnlyLongProperty compensationAmountProperty() { - if (compensationAmountProperty.get() == 0) { - compensationAmountProperty.set(model.getCompensationAmount()); - } return compensationAmountProperty; } public ReadOnlyLongProperty reimbursementAmountProperty() { - if (reimbursementAmountProperty.get() == 0) { - reimbursementAmountProperty.set(model.getReimbursementAmount()); - } return reimbursementAmountProperty; } public ReadOnlyLongProperty bsqTradeFeeAmountProperty() { - if (bsqTradeFeeAmountProperty.get() == 0) { - bsqTradeFeeAmountProperty.set(model.getBsqTradeFeeAmount()); - } return bsqTradeFeeAmountProperty; } public ReadOnlyLongProperty proofOfBurnAmountProperty() { - if (proofOfBurnAmountProperty.get() == 0) { - proofOfBurnAmountProperty.set(model.getProofOfBurnAmount()); - } return proofOfBurnAmountProperty; }