From ad1b436832c055c51228e89e216e770fa6b92459 Mon Sep 17 00:00:00 2001 From: chimp1984 Date: Mon, 4 Jan 2021 17:17:18 -0500 Subject: [PATCH 01/12] Use List instead of ObservableList, cleanups --- .../desktop/main/market/trades/TradesChartsViewModel.java | 8 ++++---- .../main/market/trades/charts/volume/VolumeBar.java | 5 ----- 2 files changed, 4 insertions(+), 9 deletions(-) diff --git a/desktop/src/main/java/bisq/desktop/main/market/trades/TradesChartsViewModel.java b/desktop/src/main/java/bisq/desktop/main/market/trades/TradesChartsViewModel.java index 0e67d26b025..a8d9c498709 100644 --- a/desktop/src/main/java/bisq/desktop/main/market/trades/TradesChartsViewModel.java +++ b/desktop/src/main/java/bisq/desktop/main/market/trades/TradesChartsViewModel.java @@ -65,6 +65,7 @@ import java.time.temporal.ChronoUnit; import java.util.ArrayList; +import java.util.Collections; import java.util.Comparator; import java.util.Date; import java.util.HashMap; @@ -291,8 +292,7 @@ CandleData getCandleData(long tick, Set set) { long accumulatedVolume = 0; long accumulatedAmount = 0; long numTrades = set.size(); - ObservableList tradePrices = FXCollections.observableArrayList(); - + List tradePrices = new ArrayList<>(); for (TradeStatistics3 item : set) { long tradePriceAsLong = item.getTradePrice().getValue(); // Previously a check was done which inverted the low and high for cryptocurrencies. @@ -301,9 +301,9 @@ CandleData getCandleData(long tick, Set set) { accumulatedVolume += item.getTradeVolume().getValue(); accumulatedAmount += item.getTradeAmount().getValue(); - tradePrices.add(item.getTradePrice().getValue()); + tradePrices.add(tradePriceAsLong); } - FXCollections.sort(tradePrices); + Collections.sort(tradePrices); List list = new ArrayList<>(set); ObservableList obsList = FXCollections.observableArrayList(list); diff --git a/desktop/src/main/java/bisq/desktop/main/market/trades/charts/volume/VolumeBar.java b/desktop/src/main/java/bisq/desktop/main/market/trades/charts/volume/VolumeBar.java index 41d45dab767..6295e75bf56 100644 --- a/desktop/src/main/java/bisq/desktop/main/market/trades/charts/volume/VolumeBar.java +++ b/desktop/src/main/java/bisq/desktop/main/market/trades/charts/volume/VolumeBar.java @@ -27,12 +27,7 @@ import javafx.util.StringConverter; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - public class VolumeBar extends Group { - private static final Logger log = LoggerFactory.getLogger(VolumeBar.class); - private String seriesStyleClass; private String dataStyleClass; private final StringConverter volumeStringConverter; From 8927787286eac3b323373be58d611ed9faf05509 Mon Sep 17 00:00:00 2001 From: chimp1984 Date: Mon, 4 Jan 2021 18:07:00 -0500 Subject: [PATCH 02/12] Cache Volume Volume was created at each access from amount and price. To improve performance we cache the result once calculated. --- .../core/trade/statistics/TradeStatistics3.java | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 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 71cb5e86023..8ef05799991 100644 --- a/core/src/main/java/bisq/core/trade/statistics/TradeStatistics3.java +++ b/core/src/main/java/bisq/core/trade/statistics/TradeStatistics3.java @@ -186,6 +186,9 @@ private enum PaymentMethodMapper { @JsonExclude private transient final Date dateObj; + @JsonExclude + private transient Volume volume = null; + public TradeStatistics3(String currency, long price, long amount, @@ -359,12 +362,15 @@ public Coin getTradeAmount() { } public Volume getTradeVolume() { - if (getTradePrice().getMonetary() instanceof Altcoin) { - return new Volume(new AltcoinExchangeRate((Altcoin) getTradePrice().getMonetary()).coinToAltcoin(getTradeAmount())); - } else { - Volume volume = new Volume(new ExchangeRate((Fiat) getTradePrice().getMonetary()).coinToFiat(getTradeAmount())); - return VolumeUtil.getRoundedFiatVolume(volume); + if (volume == null) { + if (getTradePrice().getMonetary() instanceof Altcoin) { + volume = new Volume(new AltcoinExchangeRate((Altcoin) getTradePrice().getMonetary()).coinToAltcoin(getTradeAmount())); + } else { + Volume exactVolume = new Volume(new ExchangeRate((Fiat) getTradePrice().getMonetary()).coinToFiat(getTradeAmount())); + volume = VolumeUtil.getRoundedFiatVolume(exactVolume); + } } + return volume; } public boolean isValid() { From 73fcf52129e353df7efb9f94f6f7e4e576f9b762 Mon Sep 17 00:00:00 2001 From: chimp1984 Date: Fri, 8 Jan 2021 16:38:38 -0500 Subject: [PATCH 03/12] Cache price and localDateTime --- .../trade/statistics/TradeStatistics3.java | 21 ++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) 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 8ef05799991..0c9a05ec7f5 100644 --- a/core/src/main/java/bisq/core/trade/statistics/TradeStatistics3.java +++ b/core/src/main/java/bisq/core/trade/statistics/TradeStatistics3.java @@ -50,6 +50,9 @@ import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Charsets; +import java.time.LocalDateTime; +import java.time.ZoneId; + import java.util.Arrays; import java.util.Date; import java.util.HashMap; @@ -71,6 +74,8 @@ public final class TradeStatistics3 implements ProcessOncePersistableNetworkPayload, PersistableNetworkPayload, CapabilityRequiringPayload, DateSortedTruncatablePayload { + @JsonExclude + private transient static final ZoneId ZONE_ID = ZoneId.systemDefault(); public static TradeStatistics3 from(Trade trade, @Nullable String referralId, @@ -188,6 +193,8 @@ private enum PaymentMethodMapper { @JsonExclude private transient Volume volume = null; + @JsonExclude + private transient LocalDateTime localDateTime; public TradeStatistics3(String currency, long price, @@ -331,6 +338,13 @@ public Date getDate() { return dateObj; } + public LocalDateTime getLocalDateTime() { + if (localDateTime == null) { + localDateTime = dateObj.toInstant().atZone(ZONE_ID).toLocalDateTime(); + } + return localDateTime; + } + public long getDateAsLong() { return date; } @@ -353,8 +367,13 @@ public String getPaymentMethod() { } } + private transient Price priceObj; + public Price getTradePrice() { - return Price.valueOf(currency, price); + if (priceObj == null) { + priceObj = Price.valueOf(currency, price); + } + return priceObj; } public Coin getTradeAmount() { From 764614d762aa230423de99c8ca250c5d1a6b5870 Mon Sep 17 00:00:00 2001 From: chimp1984 Date: Fri, 8 Jan 2021 16:38:58 -0500 Subject: [PATCH 04/12] Add methods for boolean values --- core/src/main/java/bisq/core/user/Cookie.java | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/core/src/main/java/bisq/core/user/Cookie.java b/core/src/main/java/bisq/core/user/Cookie.java index bc73d6bbd03..d5cb0c013f7 100644 --- a/core/src/main/java/bisq/core/user/Cookie.java +++ b/core/src/main/java/bisq/core/user/Cookie.java @@ -45,6 +45,16 @@ public Optional getAsOptionalDouble(CookieKey key) { } } + public void putAsBoolean(CookieKey key, boolean value) { + put(key, value ? "1" : "0"); + } + + public Optional getAsOptionalBoolean(CookieKey key) { + return containsKey(key) ? + Optional.of(get(key).equals("1")) : + Optional.empty(); + } + public Map toProtoMessage() { Map protoMap = new HashMap<>(); this.forEach((key, value) -> protoMap.put(key.name(), value)); From bf7acb5932d797c7d2746f5e36873dd0aab76270 Mon Sep 17 00:00:00 2001 From: chimp1984 Date: Fri, 8 Jan 2021 16:40:37 -0500 Subject: [PATCH 05/12] Add toggle for showing volume as USD instead of BTC Precalculate the USD average prices for tick intervals and use that for converting the BTC volume to USD volume. --- .../main/java/bisq/core/user/CookieKey.java | 3 +- .../resources/i18n/displayStrings.properties | 3 +- .../main/market/trades/TradesChartsView.java | 183 +++++++++++------- .../market/trades/TradesChartsViewModel.java | 130 +++++++++---- .../main/market/trades/charts/CandleData.java | 4 +- .../trades/charts/volume/VolumeBar.java | 6 +- .../java/bisq/desktop/util/DisplayUtils.java | 21 ++ .../trades/TradesChartsViewModelTest.java | 2 +- 8 files changed, 248 insertions(+), 104 deletions(-) diff --git a/core/src/main/java/bisq/core/user/CookieKey.java b/core/src/main/java/bisq/core/user/CookieKey.java index c8653e54bb2..3d39a603d2f 100644 --- a/core/src/main/java/bisq/core/user/CookieKey.java +++ b/core/src/main/java/bisq/core/user/CookieKey.java @@ -22,5 +22,6 @@ public enum CookieKey { STAGE_X, STAGE_Y, STAGE_W, - STAGE_H + STAGE_H, + TRADE_STAT_CHART_USE_USD } diff --git a/core/src/main/resources/i18n/displayStrings.properties b/core/src/main/resources/i18n/displayStrings.properties index fa728340308..69b020b98d6 100644 --- a/core/src/main/resources/i18n/displayStrings.properties +++ b/core/src/main/resources/i18n/displayStrings.properties @@ -316,7 +316,7 @@ market.spread.expanded=Expanded view # TradesChartsView market.trades.nrOfTrades=Trades: {0} -market.trades.tooltip.volumeBar=Volume: {0}\nNo. of trades: {1}\nDate: {2} +market.trades.tooltip.volumeBar=Volume: {0} / {1}\nNo. of trades: {2}\nDate: {3} market.trades.tooltip.candle.open=Open: market.trades.tooltip.candle.close=Close: market.trades.tooltip.candle.high=High: @@ -324,6 +324,7 @@ market.trades.tooltip.candle.low=Low: market.trades.tooltip.candle.average=Average: market.trades.tooltip.candle.median=Median: market.trades.tooltip.candle.date=Date: +market.trades.showVolumeInUSD=Show volume in USD #################################################################### # OfferView diff --git a/desktop/src/main/java/bisq/desktop/main/market/trades/TradesChartsView.java b/desktop/src/main/java/bisq/desktop/main/market/trades/TradesChartsView.java index 4320599881f..1b9617e975c 100644 --- a/desktop/src/main/java/bisq/desktop/main/market/trades/TradesChartsView.java +++ b/desktop/src/main/java/bisq/desktop/main/market/trades/TradesChartsView.java @@ -20,6 +20,7 @@ import bisq.desktop.common.view.ActivatableViewAndModel; import bisq.desktop.common.view.FxmlView; import bisq.desktop.components.AutoTooltipLabel; +import bisq.desktop.components.AutoTooltipSlideToggleButton; import bisq.desktop.components.AutoTooltipTableColumn; import bisq.desktop.components.AutoTooltipToggleButton; import bisq.desktop.components.AutocompleteComboBox; @@ -34,6 +35,8 @@ import bisq.core.locale.Res; import bisq.core.monetary.Price; import bisq.core.trade.statistics.TradeStatistics3; +import bisq.core.user.CookieKey; +import bisq.core.user.User; import bisq.core.util.FormattingUtils; import bisq.core.util.coin.CoinFormatter; @@ -140,8 +143,9 @@ private CurrencyListItem specialShowAllItem() { } private final CoinFormatter coinFormatter; + private final User user; - private VolumeChart volumeChart; + private VolumeChart volumeChart, volumeInUsdChart; private CandleStickChart priceChart; private AutocompleteComboBox currencyComboBox; private TableView tableView; @@ -150,6 +154,7 @@ private CurrencyListItem specialShowAllItem() { private Pane rootParent; private AnchorPane priceChartPane, volumeChartPane; private HBox footer; + private AutoTooltipSlideToggleButton showVolumeAsUsdToggleButton; private Label nrOfTradeStatisticsLabel; private ToggleGroup toggleGroup; private SingleSelectionModel tabPaneSelectionModel; @@ -172,9 +177,10 @@ private CurrencyListItem specialShowAllItem() { private Subscription currencySelectionSubscriber; private final StringProperty priceColumnLabel = new SimpleStringProperty(); - private NumberAxis priceAxisX, priceAxisY, volumeAxisY, volumeAxisX; + private NumberAxis priceAxisX, priceAxisY, volumeAxisY, volumeAxisX, volumeInUsdAxisY, volumeInUsdAxisX; private XYChart.Series priceSeries; - private XYChart.Series volumeSeries; + private final XYChart.Series volumeSeries = new XYChart.Series<>(); + private final XYChart.Series volumeInUsdSeries = new XYChart.Series<>(); private double priceAxisYWidth; private double volumeAxisYWidth; @@ -186,9 +192,11 @@ private CurrencyListItem specialShowAllItem() { @SuppressWarnings("WeakerAccess") @Inject public TradesChartsView(TradesChartsViewModel model, - @Named(FormattingUtils.BTC_FORMATTER_KEY) CoinFormatter coinFormatter) { + @Named(FormattingUtils.BTC_FORMATTER_KEY) CoinFormatter coinFormatter, + User user) { super(model); this.coinFormatter = coinFormatter; + this.user = user; } @Override @@ -221,6 +229,7 @@ public void initialize() { model.setTickUnit((TradesChartsViewModel.TickUnit) newValue.getUserData()); priceAxisX.setTickLabelFormatter(getTimeAxisStringConverter()); volumeAxisX.setTickLabelFormatter(getTimeAxisStringConverter()); + volumeInUsdAxisX.setTickLabelFormatter(getTimeAxisStringConverter()); } }; priceAxisYWidthListener = (observable, oldValue, newValue) -> { @@ -254,8 +263,10 @@ public void initialize() { tableView.getColumns().add(1, marketColumn); volumeChart.setPrefHeight(volumeChart.getMaxHeight()); + volumeInUsdChart.setPrefHeight(volumeInUsdChart.getMaxHeight()); } else { volumeChart.setPrefHeight(volumeChart.getMinHeight()); + volumeInUsdChart.setPrefHeight(volumeInUsdChart.getMinHeight()); priceSeries.setName(selectedTradeCurrency.getName()); String code = selectedTradeCurrency.getCode(); volumeColumn.setGraphic(new AutoTooltipLabel(Res.get("shared.amountWithCur", code))); @@ -267,6 +278,7 @@ public void initialize() { layout(); return null; }); + } @Override @@ -309,23 +321,21 @@ else if (model.getSelectedCurrencyListItem().isPresent()) priceAxisY.labelProperty().bind(priceColumnLabel); priceColumnLabel.addListener(priceColumnLabelListener); - currencySelectionSubscriber = currencySelectionBinding.subscribe((observable, oldValue, newValue) -> { }); - sortedList.comparatorProperty().bind(tableView.comparatorProperty()); tableView.setItems(sortedList); - priceChart.setAnimated(model.preferences.isUseAnimations()); volumeChart.setAnimated(model.preferences.isUseAnimations()); + volumeInUsdChart.setAnimated(model.preferences.isUseAnimations()); priceAxisX.setTickLabelFormatter(getTimeAxisStringConverter()); volumeAxisX.setTickLabelFormatter(getTimeAxisStringConverter()); + volumeInUsdAxisX.setTickLabelFormatter(getTimeAxisStringConverter()); nrOfTradeStatisticsLabel.setText(Res.get("market.trades.nrOfTrades", model.tradeStatisticsByCurrency.size())); exportLink.setOnAction(e -> exportToCsv()); - UserThread.runAfter(this::updateChartData, 100, TimeUnit.MILLISECONDS); if (root.getParent() instanceof Pane) { @@ -333,6 +343,17 @@ else if (model.getSelectedCurrencyListItem().isPresent()) rootParent.heightProperty().addListener(parentHeightListener); } + user.getCookie().getAsOptionalBoolean(CookieKey.TRADE_STAT_CHART_USE_USD).ifPresent(showUsd -> { + showVolumeAsUsdToggleButton.setSelected(showUsd); + showVolumeAsUsd(showUsd); + }); + showVolumeAsUsdToggleButton.setOnAction(e -> { + boolean selected = showVolumeAsUsdToggleButton.isSelected(); + showVolumeAsUsd(selected); + user.getCookie().putAsBoolean(CookieKey.TRADE_STAT_CHART_USE_USD, selected); + user.requestPersistence(); + }); + fillList(); layout(); } @@ -357,18 +378,27 @@ protected void deactivate() { priceChart.getData().clear(); exportLink.setOnAction(null); + showVolumeAsUsdToggleButton.setOnAction(null); if (rootParent != null) { rootParent.heightProperty().removeListener(parentHeightListener); } } + private void showVolumeAsUsd(Boolean showUsd) { + volumeChart.setVisible(!showUsd); + volumeChart.setManaged(!showUsd); + volumeInUsdChart.setVisible(showUsd); + volumeInUsdChart.setManaged(showUsd); + } + private void fillList() { - ObservableList tradeStatistics3ListItems = FXCollections.observableList(model.tradeStatisticsByCurrency.stream() - .map(tradeStatistics3 -> new TradeStatistics3ListItem(tradeStatistics3, - coinFormatter, - model.showAllTradeCurrenciesProperty.get())) - .collect(Collectors.toList())); + ObservableList tradeStatistics3ListItems = FXCollections.observableList( + model.tradeStatisticsByCurrency.stream() + .map(tradeStatistics -> new TradeStatistics3ListItem(tradeStatistics, + coinFormatter, + model.showAllTradeCurrenciesProperty.get())) + .collect(Collectors.toList())); listItems.clear(); listItems.addAll(tradeStatistics3ListItems); } @@ -493,24 +523,52 @@ public Number fromString(String string) { priceChartPane.getChildren().add(priceChart); - volumeSeries = new XYChart.Series<>(); - volumeAxisX = new NumberAxis(0, model.maxTicks + 1, 1); - volumeAxisX.setTickUnit(4); - volumeAxisX.setMinorTickCount(4); - volumeAxisX.setMinorTickVisible(true); - volumeAxisX.setForceZeroInRange(false); - volumeAxisX.setTickLabelFormatter(getTimeAxisStringConverter()); - addTickMarkLabelCssClass(volumeAxisX, "axis-tick-mark-text-node"); - volumeAxisY = new NumberAxis(); - volumeAxisY.setForceZeroInRange(true); - volumeAxisY.setAutoRanging(true); - volumeAxisY.setLabel(Res.get("shared.volumeWithCur", Res.getBaseCurrencyCode())); - volumeAxisY.setTickLabelFormatter(new StringConverter<>() { + volumeChart = getVolumeChart(volumeAxisX, volumeAxisY, volumeSeries, "BTC"); + + volumeInUsdAxisX = new NumberAxis(0, model.maxTicks + 1, 1); + volumeInUsdAxisY = new NumberAxis(); + volumeInUsdChart = getVolumeChart(volumeInUsdAxisX, volumeInUsdAxisY, volumeInUsdSeries, "USD"); + volumeInUsdChart.setVisible(false); + volumeInUsdChart.setManaged(false); + + showVolumeAsUsdToggleButton = new AutoTooltipSlideToggleButton(); + showVolumeAsUsdToggleButton.setText(Res.get("market.trades.showVolumeInUSD")); + showVolumeAsUsdToggleButton.setPadding(new Insets(-15, 0, 0, 10)); + + VBox vBox = new VBox(); + AnchorPane.setTopAnchor(vBox, 15d); + AnchorPane.setBottomAnchor(vBox, 10d); + AnchorPane.setLeftAnchor(vBox, 0d); + AnchorPane.setRightAnchor(vBox, 10d); + vBox.getChildren().addAll(showVolumeAsUsdToggleButton, volumeChart, volumeInUsdChart); + + volumeChartPane = new AnchorPane(); + volumeChartPane.getStyleClass().add("chart-pane"); + volumeChartPane.getChildren().add(vBox); + } + + private VolumeChart getVolumeChart(NumberAxis axisX, + NumberAxis axisY, + XYChart.Series series, + String currency) { + axisX.setTickUnit(4); + axisX.setMinorTickCount(4); + axisX.setMinorTickVisible(true); + axisX.setForceZeroInRange(false); + axisX.setTickLabelFormatter(getTimeAxisStringConverter()); + addTickMarkLabelCssClass(axisX, "axis-tick-mark-text-node"); + + axisY.setForceZeroInRange(true); + axisY.setAutoRanging(true); + axisY.setLabel(Res.get("shared.volumeWithCur", currency)); + axisY.setTickLabelFormatter(new StringConverter<>() { @Override - public String toString(Number object) { - return coinFormatter.formatCoin(Coin.valueOf(MathUtils.doubleToLong((double) object))); + public String toString(Number volume) { + return currency.equals("BTC") ? + coinFormatter.formatCoin(Coin.valueOf(MathUtils.doubleToLong((double) volume))) : + DisplayUtils.formatLargeFiatWithUnitPostFix((double) volume, "USD"); } @Override @@ -519,38 +577,31 @@ public Number fromString(String string) { } }); - volumeChart = new VolumeChart(volumeAxisX, volumeAxisY, new StringConverter<>() { + StringConverter btcStringConverter = new StringConverter<>() { @Override - public String toString(Number object) { - return coinFormatter.formatCoinWithCode(Coin.valueOf((long) object)); + public String toString(Number volume) { + return coinFormatter.formatCoinWithCode(Coin.valueOf((long) volume)); } @Override public Number fromString(String string) { return null; } - }); + }; + VolumeChart volumeChart = new VolumeChart(axisX, axisY, btcStringConverter); volumeChart.setId("volume-chart"); - volumeChart.setData(FXCollections.observableArrayList(List.of(volumeSeries))); + volumeChart.setData(FXCollections.observableArrayList(List.of(series))); volumeChart.setMinHeight(138); volumeChart.setPrefHeight(138); volumeChart.setMaxHeight(200); volumeChart.setLegendVisible(false); volumeChart.setPadding(new Insets(0)); - - volumeChartPane = new AnchorPane(); - volumeChartPane.getStyleClass().add("chart-pane"); - - AnchorPane.setTopAnchor(volumeChart, 15d); - AnchorPane.setBottomAnchor(volumeChart, 10d); - AnchorPane.setLeftAnchor(volumeChart, 0d); - AnchorPane.setRightAnchor(volumeChart, 10d); - - volumeChartPane.getChildren().add(volumeChart); + return volumeChart; } private void updateChartData() { volumeSeries.getData().setAll(model.volumeItems); + volumeInUsdSeries.getData().setAll(model.volumeInUsdItems); // At price chart we need to set the priceSeries new otherwise the lines are not rendered correctly // TODO should be fixed in candle chart @@ -566,9 +617,11 @@ private void layoutChart() { if (volumeAxisYWidth > priceAxisYWidth) { priceChart.setPadding(new Insets(0, 0, 0, volumeAxisYWidth - priceAxisYWidth)); volumeChart.setPadding(new Insets(0, 0, 0, 0)); + volumeInUsdChart.setPadding(new Insets(0, 0, 0, 0)); } else if (volumeAxisYWidth < priceAxisYWidth) { priceChart.setPadding(new Insets(0, 0, 0, 0)); volumeChart.setPadding(new Insets(0, 0, 0, priceAxisYWidth - volumeAxisYWidth)); + volumeInUsdChart.setPadding(new Insets(0, 0, 0, priceAxisYWidth - volumeAxisYWidth)); } }); } @@ -586,21 +639,21 @@ public String toString(Number object) { long time = model.getTimeFromTickIndex(index); String fmt = ""; switch (model.tickUnit) { - case YEAR: - fmt = "yyyy"; - break; - case MONTH: - fmt = "MMMyy"; - break; - case WEEK: - case DAY: - fmt = "dd/MMM\nyyyy"; - break; - case HOUR : - case MINUTE_10: - fmt = "HH:mm\ndd/MMM"; - break; - default: // nothing here + case YEAR: + fmt = "yyyy"; + break; + case MONTH: + fmt = "MMMyy"; + break; + case WEEK: + case DAY: + fmt = "dd/MMM\nyyyy"; + break; + case HOUR: + case MINUTE_10: + fmt = "HH:mm\ndd/MMM"; + break; + default: // nothing here } return DisplayUtils.formatDateAxis(new Date(time), fmt); @@ -616,16 +669,16 @@ public Number fromString(String string) { private void addTickMarkLabelCssClass(NumberAxis axis, String cssClass) { // grab the axis tick mark label (text object) and add a CSS class. 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); } } } - }); + } + }); } /////////////////////////////////////////////////////////////////////////////////////////// diff --git a/desktop/src/main/java/bisq/desktop/main/market/trades/TradesChartsViewModel.java b/desktop/src/main/java/bisq/desktop/main/market/trades/TradesChartsViewModel.java index a8d9c498709..c902ab358f5 100644 --- a/desktop/src/main/java/bisq/desktop/main/market/trades/TradesChartsViewModel.java +++ b/desktop/src/main/java/bisq/desktop/main/market/trades/TradesChartsViewModel.java @@ -61,7 +61,6 @@ import java.time.LocalDateTime; import java.time.ZoneId; -import java.time.ZonedDateTime; import java.time.temporal.ChronoUnit; import java.util.ArrayList; @@ -74,6 +73,7 @@ import java.util.Map; import java.util.Optional; import java.util.Set; +import java.util.concurrent.atomic.AtomicLong; import java.util.stream.Collectors; import javax.annotation.Nullable; @@ -81,6 +81,7 @@ class TradesChartsViewModel extends ActivatableViewModel { private static final int TAB_INDEX = 2; + private static final ZoneId ZONE_ID = ZoneId.systemDefault(); /////////////////////////////////////////////////////////////////////////////////////////// // Enum @@ -108,11 +109,14 @@ public enum TickUnit { final ObservableList tradeStatisticsByCurrency = FXCollections.observableArrayList(); final ObservableList> priceItems = FXCollections.observableArrayList(); final ObservableList> volumeItems = FXCollections.observableArrayList(); + final ObservableList> volumeInUsdItems = FXCollections.observableArrayList(); private Map>> itemsPerInterval; TickUnit tickUnit; final int maxTicks = 90; private int selectedTabIndex; + Map> usdPriceMapsPerTickUnit = new HashMap<>(); + /////////////////////////////////////////////////////////////////////////////////////////// // Constructor, lifecycle @@ -143,19 +147,11 @@ public enum TickUnit { currencyListItems = new CurrencyList(this.preferences); } - private void fillTradeCurrencies() { - // Don't use a set as we need all entries - List tradeCurrencyList = tradeStatisticsManager.getObservableTradeStatisticsSet().stream() - .flatMap(e -> CurrencyUtil.getTradeCurrency(e.getCurrency()).stream()) - .collect(Collectors.toList()); - - currencyListItems.updateWithCurrencies(tradeCurrencyList, showAllCurrencyListItem); - } - @Override protected void activate() { tradeStatisticsManager.getObservableTradeStatisticsSet().addListener(setChangeListener); fillTradeCurrencies(); + buildUsdPricesPerDay(); updateChartData(); syncPriceFeedCurrency(); setMarketPriceFeedCurrency(); @@ -226,6 +222,15 @@ public Optional getSelectedCurrencyListItem() { // Private /////////////////////////////////////////////////////////////////////////////////////////// + private void fillTradeCurrencies() { + // Don't use a set as we need all entries + List tradeCurrencyList = tradeStatisticsManager.getObservableTradeStatisticsSet().stream() + .flatMap(e -> CurrencyUtil.getTradeCurrency(e.getCurrency()).stream()) + .collect(Collectors.toList()); + + currencyListItems.updateWithCurrencies(tradeCurrencyList, showAllCurrencyListItem); + } + private void setMarketPriceFeedCurrency() { if (selectedTabIndex == TAB_INDEX) { if (showAllTradeCurrenciesProperty.get()) @@ -240,6 +245,44 @@ private void syncPriceFeedCurrency() { priceFeedService.setCurrencyCode(selectedTradeCurrencyProperty.get().getCode()); } + private void buildUsdPricesPerDay() { + if (usdPriceMapsPerTickUnit.isEmpty()) { + Map>> dateMapsPerTickUnit = new HashMap<>(); + for (TickUnit tick : TickUnit.values()) { + dateMapsPerTickUnit.put(tick, new HashMap<>()); + } + + tradeStatisticsManager.getObservableTradeStatisticsSet().stream() + .filter(e -> e.getCurrency().equals("USD")) + .forEach(tradeStatistics -> { + for (TickUnit tick : TickUnit.values()) { + long time = roundToTick(tradeStatistics.getLocalDateTime(), tick).getTime(); + Map> map = dateMapsPerTickUnit.get(tick); + map.putIfAbsent(time, new ArrayList<>()); + map.get(time).add(tradeStatistics); + } + }); + + dateMapsPerTickUnit.forEach((tick, map) -> { + HashMap priceMap = new HashMap<>(); + map.forEach((date, tradeStatisticsList) -> priceMap.put(date, getAveragePrice(tradeStatisticsList))); + usdPriceMapsPerTickUnit.put(tick, priceMap); + }); + } + } + + private long getAveragePrice(List tradeStatisticsList) { + long accumulatedAmount = 0; + long accumulatedVolume = 0; + for (TradeStatistics3 tradeStatistics : tradeStatisticsList) { + accumulatedAmount += tradeStatistics.getAmount(); + accumulatedVolume += tradeStatistics.getTradeVolume().getValue(); + } + + double accumulatedVolumeAsDouble = MathUtils.scaleUpByPowerOf10((double) accumulatedVolume, Coin.SMALLEST_UNIT_EXPONENT); + return MathUtils.roundDoubleToLong(accumulatedVolumeAsDouble / (double) accumulatedAmount); + } + private void updateChartData() { tradeStatisticsByCurrency.setAll(tradeStatisticsManager.getObservableTradeStatisticsSet().stream() .filter(e -> showAllTradeCurrenciesProperty.get() || e.getCurrency().equals(getCurrencyCode())) @@ -249,28 +292,38 @@ private void updateChartData() { itemsPerInterval = new HashMap<>(); Date time = new Date(); for (long i = maxTicks + 1; i >= 0; --i) { - Set set = new HashSet<>(); - Pair> pair = new Pair<>((Date) time.clone(), set); + Pair> pair = new Pair<>((Date) time.clone(), new HashSet<>()); itemsPerInterval.put(i, pair); + // We adjust the time for the next iteration time.setTime(time.getTime() - 1); time = roundToTick(time, tickUnit); } // Get all entries for the defined time interval - tradeStatisticsByCurrency.forEach(e -> { + tradeStatisticsByCurrency.forEach(tradeStatistics -> { for (long i = maxTicks; i > 0; --i) { - Pair> p = itemsPerInterval.get(i); - if (e.getDate().after(p.getKey())) { - p.getValue().add(e); + Pair> pair = itemsPerInterval.get(i); + if (tradeStatistics.getDate().after(pair.getKey())) { + pair.getValue().add(tradeStatistics); break; } } }); + Map map = usdPriceMapsPerTickUnit.get(tickUnit); + AtomicLong averageUsdPrice = new AtomicLong(0); + // create CandleData for defined time interval List candleDataList = itemsPerInterval.entrySet().stream() .filter(entry -> entry.getKey() >= 0 && !entry.getValue().getValue().isEmpty()) - .map(entry -> getCandleData(entry.getKey(), entry.getValue().getValue())) + .map(entry -> { + long tickStartDate = entry.getValue().getKey().getTime(); + // If we don't have a price we take the previous one + if (map.containsKey(tickStartDate)) { + averageUsdPrice.set(map.get(tickStartDate)); + } + return getCandleData(entry.getKey(), entry.getValue().getValue(), averageUsdPrice.get()); + }) .sorted(Comparator.comparingLong(o -> o.tick)) .collect(Collectors.toList()); @@ -279,12 +332,16 @@ private void updateChartData() { .collect(Collectors.toList())); volumeItems.setAll(candleDataList.stream() - .map(e -> new XYChart.Data(e.tick, e.accumulatedAmount, e)) + .map(candleData -> new XYChart.Data(candleData.tick, candleData.accumulatedAmount, candleData)) + .collect(Collectors.toList())); + + volumeInUsdItems.setAll(candleDataList.stream() + .map(candleData -> new XYChart.Data(candleData.tick, candleData.volumeInUsd, candleData)) .collect(Collectors.toList())); } @VisibleForTesting - CandleData getCandleData(long tick, Set set) { + CandleData getCandleData(long tick, Set set, long averageUsdPrice) { long open = 0; long close = 0; long high = 0; @@ -328,36 +385,43 @@ CandleData getCandleData(long tick, Set set) { averagePrice = MathUtils.roundDoubleToLong(accumulatedVolumeAsDouble / (double) accumulatedAmount); } - final Date dateFrom = new Date(getTimeFromTickIndex(tick)); - final Date dateTo = new Date(getTimeFromTickIndex(tick + 1)); + Date dateFrom = new Date(getTimeFromTickIndex(tick)); + Date dateTo = new Date(getTimeFromTickIndex(tick + 1)); String dateString = tickUnit.ordinal() > TickUnit.DAY.ordinal() ? DisplayUtils.formatDateTimeSpan(dateFrom, dateTo) : DisplayUtils.formatDate(dateFrom) + " - " + DisplayUtils.formatDate(dateTo); + + // We do not need precision, so we scale down before multiplication otherwise we could get an overflow. + averageUsdPrice = (long) MathUtils.scaleDownByPowerOf10((double) averageUsdPrice, 4); + long volumeInUsd = averageUsdPrice * (long) MathUtils.scaleDownByPowerOf10((double) accumulatedAmount, 4); + // We store USD value without decimals as its only total volume, no precision is needed. + volumeInUsd = (long) MathUtils.scaleDownByPowerOf10((double) volumeInUsd, 4); return new CandleData(tick, open, close, high, low, averagePrice, medianPrice, accumulatedAmount, accumulatedVolume, - numTrades, isBullish, dateString); + numTrades, isBullish, dateString, volumeInUsd); } Date roundToTick(Date time, TickUnit tickUnit) { - ZonedDateTime zdt = time.toInstant().atZone(ZoneId.systemDefault()); - LocalDateTime tradeLocal = zdt.toLocalDateTime(); + return roundToTick(time.toInstant().atZone(ZONE_ID).toLocalDateTime(), tickUnit); + } + Date roundToTick(LocalDateTime localDate, TickUnit tickUnit) { switch (tickUnit) { case YEAR: - return Date.from(tradeLocal.withMonth(1).withDayOfYear(1).withHour(0).withMinute(0).withSecond(0).withNano(0).atZone(ZoneId.systemDefault()).toInstant()); + return Date.from(localDate.withMonth(1).withDayOfYear(1).withHour(0).withMinute(0).withSecond(0).withNano(0).atZone(ZONE_ID).toInstant()); case MONTH: - return Date.from(tradeLocal.withDayOfMonth(1).withHour(0).withMinute(0).withSecond(0).withNano(0).atZone(ZoneId.systemDefault()).toInstant()); + return Date.from(localDate.withDayOfMonth(1).withHour(0).withMinute(0).withSecond(0).withNano(0).atZone(ZONE_ID).toInstant()); case WEEK: - int dayOfWeek = tradeLocal.getDayOfWeek().getValue(); - LocalDateTime firstDayOfWeek = ChronoUnit.DAYS.addTo(tradeLocal, 1 - dayOfWeek); - return Date.from(firstDayOfWeek.withHour(0).withMinute(0).withSecond(0).withNano(0).atZone(ZoneId.systemDefault()).toInstant()); + int dayOfWeek = localDate.getDayOfWeek().getValue(); + LocalDateTime firstDayOfWeek = ChronoUnit.DAYS.addTo(localDate, 1 - dayOfWeek); + return Date.from(firstDayOfWeek.withHour(0).withMinute(0).withSecond(0).withNano(0).atZone(ZONE_ID).toInstant()); case DAY: - return Date.from(tradeLocal.withHour(0).withMinute(0).withSecond(0).withNano(0).atZone(ZoneId.systemDefault()).toInstant()); + return Date.from(localDate.withHour(0).withMinute(0).withSecond(0).withNano(0).atZone(ZONE_ID).toInstant()); case HOUR: - return Date.from(tradeLocal.withMinute(0).withSecond(0).withNano(0).atZone(ZoneId.systemDefault()).toInstant()); + return Date.from(localDate.withMinute(0).withSecond(0).withNano(0).atZone(ZONE_ID).toInstant()); case MINUTE_10: - return Date.from(tradeLocal.withMinute(tradeLocal.getMinute() - tradeLocal.getMinute() % 10).withSecond(0).withNano(0).atZone(ZoneId.systemDefault()).toInstant()); + return Date.from(localDate.withMinute(localDate.getMinute() - localDate.getMinute() % 10).withSecond(0).withNano(0).atZone(ZONE_ID).toInstant()); default: - return Date.from(tradeLocal.atZone(ZoneId.systemDefault()).toInstant()); + return Date.from(localDate.atZone(ZONE_ID).toInstant()); } } diff --git a/desktop/src/main/java/bisq/desktop/main/market/trades/charts/CandleData.java b/desktop/src/main/java/bisq/desktop/main/market/trades/charts/CandleData.java index 328d8a51397..59fc321b19d 100644 --- a/desktop/src/main/java/bisq/desktop/main/market/trades/charts/CandleData.java +++ b/desktop/src/main/java/bisq/desktop/main/market/trades/charts/CandleData.java @@ -30,10 +30,11 @@ public class CandleData { public final long numTrades; public final boolean isBullish; public final String date; + public final long volumeInUsd; public CandleData(long tick, long open, long close, long high, long low, long average, long median, long accumulatedAmount, long accumulatedVolume, long numTrades, - boolean isBullish, String date) { + boolean isBullish, String date, long volumeInUsd) { this.tick = tick; this.open = open; this.close = close; @@ -46,5 +47,6 @@ public CandleData(long tick, long open, long close, long high, long low, long av this.numTrades = numTrades; this.isBullish = isBullish; this.date = date; + this.volumeInUsd = volumeInUsd; } } diff --git a/desktop/src/main/java/bisq/desktop/main/market/trades/charts/volume/VolumeBar.java b/desktop/src/main/java/bisq/desktop/main/market/trades/charts/volume/VolumeBar.java index 6295e75bf56..b6637cce7c8 100644 --- a/desktop/src/main/java/bisq/desktop/main/market/trades/charts/volume/VolumeBar.java +++ b/desktop/src/main/java/bisq/desktop/main/market/trades/charts/volume/VolumeBar.java @@ -18,6 +18,7 @@ package bisq.desktop.main.market.trades.charts.volume; import bisq.desktop.main.market.trades.charts.CandleData; +import bisq.desktop.util.DisplayUtils; import bisq.core.locale.Res; @@ -55,8 +56,9 @@ public void setSeriesAndDataStyleClasses(String seriesStyleClass, String dataSty public void update(double height, double candleWidth, CandleData candleData) { bar.resizeRelocate(-candleWidth / 2, 0, candleWidth, height); - String vol = volumeStringConverter.toString(candleData.accumulatedAmount); - tooltip.setText(Res.get("market.trades.tooltip.volumeBar", vol, candleData.numTrades, candleData.date)); + String volumeInBtc = volumeStringConverter.toString(candleData.accumulatedAmount); + String volumeInUsd = DisplayUtils.formatLargeFiat(candleData.volumeInUsd, "USD"); + tooltip.setText(Res.get("market.trades.tooltip.volumeBar", volumeInBtc, volumeInUsd, candleData.numTrades, candleData.date)); } private void updateStyleClasses() { diff --git a/desktop/src/main/java/bisq/desktop/util/DisplayUtils.java b/desktop/src/main/java/bisq/desktop/util/DisplayUtils.java index f8d144e6fa9..21ff4f1f8ba 100644 --- a/desktop/src/main/java/bisq/desktop/util/DisplayUtils.java +++ b/desktop/src/main/java/bisq/desktop/util/DisplayUtils.java @@ -21,12 +21,15 @@ import org.apache.commons.lang3.time.DurationFormatUtils; import java.text.DateFormat; +import java.text.DecimalFormat; +import java.text.NumberFormat; import java.text.SimpleDateFormat; import java.math.BigDecimal; import java.math.RoundingMode; import java.util.Date; +import java.util.Locale; import java.util.Optional; import lombok.extern.slf4j.Slf4j; @@ -106,6 +109,24 @@ public static String formatVolume(Offer offer, Boolean decimalAligned, int maxNu return formattedVolume; } + public static String formatLargeFiat(double value, String currency) { + if (value <= 0) { + return "0"; + } + NumberFormat numberFormat = DecimalFormat.getInstance(Locale.US); + numberFormat.setGroupingUsed(true); + return numberFormat.format(value) + " " + currency; + } + + public static String formatLargeFiatWithUnitPostFix(double value, String currency) { + if (value <= 0) { + return "0"; + } + String[] units = new String[]{"", "K", "M", "B"}; + int digitGroups = (int) (Math.log10(value) / Math.log10(1000)); + return new DecimalFormat("#,##0.###").format(value / Math.pow(1000, digitGroups)) + units[digitGroups] + " " + currency; + } + public static String formatVolume(Volume volume) { return formatVolume(volume, FIAT_VOLUME_FORMAT, false); } diff --git a/desktop/src/test/java/bisq/desktop/main/market/trades/TradesChartsViewModelTest.java b/desktop/src/test/java/bisq/desktop/main/market/trades/TradesChartsViewModelTest.java index 8d3a7d25660..470c86b853b 100644 --- a/desktop/src/test/java/bisq/desktop/main/market/trades/TradesChartsViewModelTest.java +++ b/desktop/src/test/java/bisq/desktop/main/market/trades/TradesChartsViewModelTest.java @@ -167,7 +167,7 @@ public void testGetCandleData() { null, null)); - CandleData candleData = model.getCandleData(model.roundToTick(now, TradesChartsViewModel.TickUnit.DAY).getTime(), set); + CandleData candleData = model.getCandleData(model.roundToTick(now, TradesChartsViewModel.TickUnit.DAY).getTime(), set, 0); assertEquals(open, candleData.open); assertEquals(close, candleData.close); assertEquals(high, candleData.high); From 5ec6ad503fbf203ef4cf5e7576ee228d5a55a58c Mon Sep 17 00:00:00 2001 From: chimp1984 Date: Fri, 8 Jan 2021 18:24:56 -0500 Subject: [PATCH 06/12] Cache fields in TradeStatistics3ListItem --- .../trades/TradeStatistics3ListItem.java | 42 ++++++++++++++----- 1 file changed, 32 insertions(+), 10 deletions(-) diff --git a/desktop/src/main/java/bisq/desktop/main/market/trades/TradeStatistics3ListItem.java b/desktop/src/main/java/bisq/desktop/main/market/trades/TradeStatistics3ListItem.java index 80fdcb922fc..234342d0727 100644 --- a/desktop/src/main/java/bisq/desktop/main/market/trades/TradeStatistics3ListItem.java +++ b/desktop/src/main/java/bisq/desktop/main/market/trades/TradeStatistics3ListItem.java @@ -34,6 +34,12 @@ public class TradeStatistics3ListItem { private final TradeStatistics3 tradeStatistics3; private final CoinFormatter coinFormatter; private final boolean showAllTradeCurrencies; + private String dateString; + private String market; + private String priceString; + private String volumeString; + private String paymentMethodString; + private String amountString; public TradeStatistics3ListItem(@Nullable TradeStatistics3 tradeStatistics3, CoinFormatter coinFormatter, @@ -44,31 +50,47 @@ public TradeStatistics3ListItem(@Nullable TradeStatistics3 tradeStatistics3, } public String getDateString() { - return tradeStatistics3 != null ? DisplayUtils.formatDateTime(tradeStatistics3.getDate()) : ""; + if (dateString == null) { + dateString = tradeStatistics3 != null ? DisplayUtils.formatDateTime(tradeStatistics3.getDate()) : ""; + } + return dateString; } public String getMarket() { - return tradeStatistics3 != null ? CurrencyUtil.getCurrencyPair(tradeStatistics3.getCurrency()) : ""; + if (market == null) { + market = tradeStatistics3 != null ? CurrencyUtil.getCurrencyPair(tradeStatistics3.getCurrency()) : ""; + } + return market; } public String getPriceString() { - return tradeStatistics3 != null ? FormattingUtils.formatPrice(tradeStatistics3.getTradePrice()) : ""; + if (priceString == null) { + priceString = tradeStatistics3 != null ? FormattingUtils.formatPrice(tradeStatistics3.getTradePrice()) : ""; + } + return priceString; } public String getVolumeString() { - if (tradeStatistics3 == null) { - return ""; + if (volumeString == null) { + volumeString = tradeStatistics3 != null ? showAllTradeCurrencies ? + DisplayUtils.formatVolumeWithCode(tradeStatistics3.getTradeVolume()) : + DisplayUtils.formatVolume(tradeStatistics3.getTradeVolume()) + : ""; } - return showAllTradeCurrencies ? - DisplayUtils.formatVolumeWithCode(tradeStatistics3.getTradeVolume()) : - DisplayUtils.formatVolume(tradeStatistics3.getTradeVolume()); + return volumeString; } public String getPaymentMethodString() { - return tradeStatistics3 != null ? Res.get(tradeStatistics3.getPaymentMethod()) : ""; + if (paymentMethodString == null) { + paymentMethodString = tradeStatistics3 != null ? Res.get(tradeStatistics3.getPaymentMethod()) : ""; + } + return paymentMethodString; } public String getAmountString() { - return tradeStatistics3 != null ? coinFormatter.formatCoin(tradeStatistics3.getTradeAmount(), 4) : ""; + if (amountString == null) { + amountString = tradeStatistics3 != null ? coinFormatter.formatCoin(tradeStatistics3.getTradeAmount(), 4) : ""; + } + return amountString; } } From 0d09c19953cbb6b8b6a206d2463eae07be332233 Mon Sep 17 00:00:00 2001 From: chimp1984 Date: Fri, 8 Jan 2021 19:30:10 -0500 Subject: [PATCH 07/12] Cleanups --- .../desktop/main/market/trades/TradesChartsViewModel.java | 2 +- .../desktop/main/market/trades/charts/price/Candle.java | 5 ----- .../main/market/trades/charts/price/CandleStickChart.java | 6 +++--- .../main/market/trades/charts/volume/VolumeChart.java | 6 +++--- 4 files changed, 7 insertions(+), 12 deletions(-) diff --git a/desktop/src/main/java/bisq/desktop/main/market/trades/TradesChartsViewModel.java b/desktop/src/main/java/bisq/desktop/main/market/trades/TradesChartsViewModel.java index c902ab358f5..bb21a22314a 100644 --- a/desktop/src/main/java/bisq/desktop/main/market/trades/TradesChartsViewModel.java +++ b/desktop/src/main/java/bisq/desktop/main/market/trades/TradesChartsViewModel.java @@ -115,7 +115,7 @@ public enum TickUnit { TickUnit tickUnit; final int maxTicks = 90; private int selectedTabIndex; - Map> usdPriceMapsPerTickUnit = new HashMap<>(); + final Map> usdPriceMapsPerTickUnit = new HashMap<>(); /////////////////////////////////////////////////////////////////////////////////////////// diff --git a/desktop/src/main/java/bisq/desktop/main/market/trades/charts/price/Candle.java b/desktop/src/main/java/bisq/desktop/main/market/trades/charts/price/Candle.java index 394a5a4d23f..bad1ed721fb 100644 --- a/desktop/src/main/java/bisq/desktop/main/market/trades/charts/price/Candle.java +++ b/desktop/src/main/java/bisq/desktop/main/market/trades/charts/price/Candle.java @@ -58,15 +58,10 @@ import javafx.util.StringConverter; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - /** * Candle node used for drawing a candle */ public class Candle extends Group { - private static final Logger log = LoggerFactory.getLogger(Candle.class); - private String seriesStyleClass; private String dataStyleClass; private final CandleTooltip candleTooltip; diff --git a/desktop/src/main/java/bisq/desktop/main/market/trades/charts/price/CandleStickChart.java b/desktop/src/main/java/bisq/desktop/main/market/trades/charts/price/CandleStickChart.java index 2403ac4fc7f..993bb93992b 100644 --- a/desktop/src/main/java/bisq/desktop/main/market/trades/charts/price/CandleStickChart.java +++ b/desktop/src/main/java/bisq/desktop/main/market/trades/charts/price/CandleStickChart.java @@ -108,14 +108,14 @@ protected void layoutPlotChildren() { // update candle positions for (int seriesIndex = 0; seriesIndex < getData().size(); seriesIndex++) { XYChart.Series series = getData().get(seriesIndex); - Iterator> iter = getDisplayedDataIterator(series); + Iterator> iterator = getDisplayedDataIterator(series); Path seriesPath = null; if (series.getNode() instanceof Path) { seriesPath = (Path) series.getNode(); seriesPath.getElements().clear(); } - while (iter.hasNext()) { - XYChart.Data item = iter.next(); + while (iterator.hasNext()) { + XYChart.Data item = iterator.next(); double x = getXAxis().getDisplayPosition(getCurrentDisplayedXValue(item)); double y = getYAxis().getDisplayPosition(getCurrentDisplayedYValue(item)); Node itemNode = item.getNode(); diff --git a/desktop/src/main/java/bisq/desktop/main/market/trades/charts/volume/VolumeChart.java b/desktop/src/main/java/bisq/desktop/main/market/trades/charts/volume/VolumeChart.java index d5a0f456da4..581fbadf0f1 100644 --- a/desktop/src/main/java/bisq/desktop/main/market/trades/charts/volume/VolumeChart.java +++ b/desktop/src/main/java/bisq/desktop/main/market/trades/charts/volume/VolumeChart.java @@ -53,9 +53,9 @@ protected void layoutPlotChildren() { } for (int seriesIndex = 0; seriesIndex < getData().size(); seriesIndex++) { XYChart.Series series = getData().get(seriesIndex); - Iterator> iter = getDisplayedDataIterator(series); - while (iter.hasNext()) { - XYChart.Data item = iter.next(); + Iterator> iterator = getDisplayedDataIterator(series); + while (iterator.hasNext()) { + XYChart.Data item = iterator.next(); double x = getXAxis().getDisplayPosition(getCurrentDisplayedXValue(item)); double y = getYAxis().getDisplayPosition(getCurrentDisplayedYValue(item)); Node itemNode = item.getNode(); From b3c8e20d66b93b050318b6d360a76a7fd7d7c68c Mon Sep 17 00:00:00 2001 From: chimp1984 Date: Fri, 8 Jan 2021 19:31:33 -0500 Subject: [PATCH 08/12] Fix available height calculation Fix incorrect removeListener call on priceColumn.textProperty() instead of priceColumnLabel --- .../main/market/trades/TradesChartsView.java | 66 ++++++++++++------- 1 file changed, 43 insertions(+), 23 deletions(-) diff --git a/desktop/src/main/java/bisq/desktop/main/market/trades/TradesChartsView.java b/desktop/src/main/java/bisq/desktop/main/market/trades/TradesChartsView.java index 1b9617e975c..37b5ece125d 100644 --- a/desktop/src/main/java/bisq/desktop/main/market/trades/TradesChartsView.java +++ b/desktop/src/main/java/bisq/desktop/main/market/trades/TradesChartsView.java @@ -142,8 +142,8 @@ private CurrencyListItem specialShowAllItem() { } } - private final CoinFormatter coinFormatter; private final User user; + private final CoinFormatter coinFormatter; private VolumeChart volumeChart, volumeInUsdChart; private CandleStickChart priceChart; @@ -177,7 +177,7 @@ private CurrencyListItem specialShowAllItem() { private Subscription currencySelectionSubscriber; private final StringProperty priceColumnLabel = new SimpleStringProperty(); - private NumberAxis priceAxisX, priceAxisY, volumeAxisY, volumeAxisX, volumeInUsdAxisY, volumeInUsdAxisX; + private NumberAxis priceAxisX, priceAxisY, volumeAxisY, volumeAxisX, volumeInUsdAxisX; private XYChart.Series priceSeries; private final XYChart.Series volumeSeries = new XYChart.Series<>(); private final XYChart.Series volumeInUsdSeries = new XYChart.Series<>(); @@ -192,11 +192,11 @@ private CurrencyListItem specialShowAllItem() { @SuppressWarnings("WeakerAccess") @Inject public TradesChartsView(TradesChartsViewModel model, - @Named(FormattingUtils.BTC_FORMATTER_KEY) CoinFormatter coinFormatter, - User user) { + User user, + @Named(FormattingUtils.BTC_FORMATTER_KEY) CoinFormatter coinFormatter) { super(model); - this.coinFormatter = coinFormatter; this.user = user; + this.coinFormatter = coinFormatter; } @Override @@ -275,6 +275,7 @@ public void initialize() { tableView.getColumns().remove(marketColumn); } + layout(); return null; }); @@ -325,10 +326,11 @@ else if (model.getSelectedCurrencyListItem().isPresent()) }); sortedList.comparatorProperty().bind(tableView.comparatorProperty()); - tableView.setItems(sortedList); - priceChart.setAnimated(model.preferences.isUseAnimations()); - volumeChart.setAnimated(model.preferences.isUseAnimations()); - volumeInUsdChart.setAnimated(model.preferences.isUseAnimations()); + + boolean useAnimations = model.preferences.isUseAnimations(); + priceChart.setAnimated(useAnimations); + volumeChart.setAnimated(useAnimations); + volumeInUsdChart.setAnimated(useAnimations); priceAxisX.setTickLabelFormatter(getTimeAxisStringConverter()); volumeAxisX.setTickLabelFormatter(getTimeAxisStringConverter()); volumeInUsdAxisX.setTickLabelFormatter(getTimeAxisStringConverter()); @@ -355,6 +357,7 @@ else if (model.getSelectedCurrencyListItem().isPresent()) }); fillList(); + tableView.setItems(sortedList); layout(); } @@ -368,7 +371,7 @@ protected void deactivate() { model.tradeStatisticsByCurrency.removeListener(tradeStatisticsByCurrencyListener); priceAxisY.labelProperty().unbind(); - priceColumn.textProperty().removeListener(priceColumnLabelListener); + priceColumnLabel.removeListener(priceColumnLabelListener); currencySelectionSubscriber.unsubscribe(); @@ -528,7 +531,7 @@ public Number fromString(String string) { volumeChart = getVolumeChart(volumeAxisX, volumeAxisY, volumeSeries, "BTC"); volumeInUsdAxisX = new NumberAxis(0, model.maxTicks + 1, 1); - volumeInUsdAxisY = new NumberAxis(); + NumberAxis volumeInUsdAxisY = new NumberAxis(); volumeInUsdChart = getVolumeChart(volumeInUsdAxisX, volumeInUsdAxisY, volumeInUsdSeries, "USD"); volumeInUsdChart.setVisible(false); volumeInUsdChart.setManaged(false); @@ -902,19 +905,36 @@ public void updateItem(final TradeStatistics3ListItem item, boolean empty) { } private void layout() { - UserThread.runAfter(() -> { - double available; - if (root.getParent() instanceof Pane) { - available = ((Pane) root.getParent()).getHeight(); - } else { - available = root.getHeight(); - } + double available; + if (root.getParent() instanceof Pane) { + available = ((Pane) root.getParent()).getHeight(); + } else { + available = root.getHeight(); + } + if (available == 0) { + UserThread.execute(this::layout); + return; + } - available = available - volumeChartPane.getHeight() - toolBox.getHeight() - footer.getHeight() - 60; - if (priceChart.isManaged()) { - available = available - priceChartPane.getHeight(); + available = available - volumeChartPane.getHeight() - toolBox.getHeight() - footer.getHeight() - 60; + + if (!model.showAllTradeCurrenciesProperty.get()) { + double priceChartPaneHeight = priceChartPane.getHeight(); + if (priceChartPaneHeight == 0) { + UserThread.execute(this::layout); + return; + } + available -= priceChartPaneHeight; + } else { + // If rendering is not done we get the height which is smaller than the volumeChart max Height so we + // delay to next render frame. + // Using runAfter does not work well as filling the table list and creating the chart can be a bit slow and + // its hard to estimate correct delay. + if (volumeChartPane.getHeight() < volumeChart.getMaxHeight()) { + UserThread.execute(this::layout); + return; } - tableView.setPrefHeight(available); - }, 100, TimeUnit.MILLISECONDS); + } + tableView.setPrefHeight(available); } } From 397a97a8993bbe6f10ce4ae7585135a970177097 Mon Sep 17 00:00:00 2001 From: chimp1984 Date: Fri, 8 Jan 2021 19:42:51 -0500 Subject: [PATCH 09/12] Call fillTradeCurrencies only once at activate --- .../main/market/trades/TradesChartsViewModel.java | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/desktop/src/main/java/bisq/desktop/main/market/trades/TradesChartsViewModel.java b/desktop/src/main/java/bisq/desktop/main/market/trades/TradesChartsViewModel.java index bb21a22314a..5d4f9842818 100644 --- a/desktop/src/main/java/bisq/desktop/main/market/trades/TradesChartsViewModel.java +++ b/desktop/src/main/java/bisq/desktop/main/market/trades/TradesChartsViewModel.java @@ -83,6 +83,7 @@ class TradesChartsViewModel extends ActivatableViewModel { private static final int TAB_INDEX = 2; private static final ZoneId ZONE_ID = ZoneId.systemDefault(); + /////////////////////////////////////////////////////////////////////////////////////////// // Enum /////////////////////////////////////////////////////////////////////////////////////////// @@ -116,7 +117,7 @@ public enum TickUnit { final int maxTicks = 90; private int selectedTabIndex; final Map> usdPriceMapsPerTickUnit = new HashMap<>(); - + private boolean fillTradeCurrenciesOnActiavetCalled; /////////////////////////////////////////////////////////////////////////////////////////// // Constructor, lifecycle @@ -150,7 +151,10 @@ public enum TickUnit { @Override protected void activate() { tradeStatisticsManager.getObservableTradeStatisticsSet().addListener(setChangeListener); - fillTradeCurrencies(); + if (!fillTradeCurrenciesOnActiavetCalled) { + fillTradeCurrencies(); + fillTradeCurrenciesOnActiavetCalled = true; + } buildUsdPricesPerDay(); updateChartData(); syncPriceFeedCurrency(); @@ -284,8 +288,9 @@ private long getAveragePrice(List tradeStatisticsList) { } private void updateChartData() { + String currencyCode = getCurrencyCode(); tradeStatisticsByCurrency.setAll(tradeStatisticsManager.getObservableTradeStatisticsSet().stream() - .filter(e -> showAllTradeCurrenciesProperty.get() || e.getCurrency().equals(getCurrencyCode())) + .filter(e -> showAllTradeCurrenciesProperty.get() || e.getCurrency().equals(currencyCode)) .collect(Collectors.toList())); // Generate date range and create sets for all ticks From 2006f58580c3186cd4aa21c1ab84387d026902fb Mon Sep 17 00:00:00 2001 From: chimp1984 Date: Fri, 8 Jan 2021 19:43:24 -0500 Subject: [PATCH 10/12] Refactor: Rename tradeStatisticsByCurrency to selectedTradeStatistics --- .../desktop/main/market/trades/TradesChartsView.java | 10 +++++----- .../main/market/trades/TradesChartsViewModel.java | 6 +++--- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/desktop/src/main/java/bisq/desktop/main/market/trades/TradesChartsView.java b/desktop/src/main/java/bisq/desktop/main/market/trades/TradesChartsView.java index 37b5ece125d..b42e2c3a435 100644 --- a/desktop/src/main/java/bisq/desktop/main/market/trades/TradesChartsView.java +++ b/desktop/src/main/java/bisq/desktop/main/market/trades/TradesChartsView.java @@ -241,7 +241,7 @@ public void initialize() { layoutChart(); }; tradeStatisticsByCurrencyListener = c -> nrOfTradeStatisticsLabel.setText(Res.get("market.trades.nrOfTrades", - model.tradeStatisticsByCurrency.size())); + model.selectedTradeStatistics.size())); parentHeightListener = (observable, oldValue, newValue) -> layout(); priceColumnLabelListener = (o, oldVal, newVal) -> priceColumn.setGraphic(new AutoTooltipLabel(newVal)); @@ -318,7 +318,7 @@ else if (model.getSelectedCurrencyListItem().isPresent()) toggleGroup.selectedToggleProperty().addListener(timeUnitChangeListener); priceAxisY.widthProperty().addListener(priceAxisYWidthListener); volumeAxisY.widthProperty().addListener(volumeAxisYWidthListener); - model.tradeStatisticsByCurrency.addListener(tradeStatisticsByCurrencyListener); + model.selectedTradeStatistics.addListener(tradeStatisticsByCurrencyListener); priceAxisY.labelProperty().bind(priceColumnLabel); priceColumnLabel.addListener(priceColumnLabelListener); @@ -335,7 +335,7 @@ else if (model.getSelectedCurrencyListItem().isPresent()) volumeAxisX.setTickLabelFormatter(getTimeAxisStringConverter()); volumeInUsdAxisX.setTickLabelFormatter(getTimeAxisStringConverter()); - nrOfTradeStatisticsLabel.setText(Res.get("market.trades.nrOfTrades", model.tradeStatisticsByCurrency.size())); + nrOfTradeStatisticsLabel.setText(Res.get("market.trades.nrOfTrades", model.selectedTradeStatistics.size())); exportLink.setOnAction(e -> exportToCsv()); UserThread.runAfter(this::updateChartData, 100, TimeUnit.MILLISECONDS); @@ -368,7 +368,7 @@ protected void deactivate() { toggleGroup.selectedToggleProperty().removeListener(timeUnitChangeListener); priceAxisY.widthProperty().removeListener(priceAxisYWidthListener); volumeAxisY.widthProperty().removeListener(volumeAxisYWidthListener); - model.tradeStatisticsByCurrency.removeListener(tradeStatisticsByCurrencyListener); + model.selectedTradeStatistics.removeListener(tradeStatisticsByCurrencyListener); priceAxisY.labelProperty().unbind(); priceColumnLabel.removeListener(priceColumnLabelListener); @@ -397,7 +397,7 @@ private void showVolumeAsUsd(Boolean showUsd) { private void fillList() { ObservableList tradeStatistics3ListItems = FXCollections.observableList( - model.tradeStatisticsByCurrency.stream() + model.selectedTradeStatistics.stream() .map(tradeStatistics -> new TradeStatistics3ListItem(tradeStatistics, coinFormatter, model.showAllTradeCurrenciesProperty.get())) diff --git a/desktop/src/main/java/bisq/desktop/main/market/trades/TradesChartsViewModel.java b/desktop/src/main/java/bisq/desktop/main/market/trades/TradesChartsViewModel.java index 5d4f9842818..904f3a00d2e 100644 --- a/desktop/src/main/java/bisq/desktop/main/market/trades/TradesChartsViewModel.java +++ b/desktop/src/main/java/bisq/desktop/main/market/trades/TradesChartsViewModel.java @@ -107,7 +107,7 @@ public enum TickUnit { final BooleanProperty showAllTradeCurrenciesProperty = new SimpleBooleanProperty(false); private final CurrencyList currencyListItems; private final CurrencyListItem showAllCurrencyListItem = new CurrencyListItem(new CryptoCurrency(GUIUtil.SHOW_ALL_FLAG, ""), -1); - final ObservableList tradeStatisticsByCurrency = FXCollections.observableArrayList(); + final ObservableList selectedTradeStatistics = FXCollections.observableArrayList(); final ObservableList> priceItems = FXCollections.observableArrayList(); final ObservableList> volumeItems = FXCollections.observableArrayList(); final ObservableList> volumeInUsdItems = FXCollections.observableArrayList(); @@ -289,7 +289,7 @@ private long getAveragePrice(List tradeStatisticsList) { private void updateChartData() { String currencyCode = getCurrencyCode(); - tradeStatisticsByCurrency.setAll(tradeStatisticsManager.getObservableTradeStatisticsSet().stream() + selectedTradeStatistics.setAll(tradeStatisticsManager.getObservableTradeStatisticsSet().stream() .filter(e -> showAllTradeCurrenciesProperty.get() || e.getCurrency().equals(currencyCode)) .collect(Collectors.toList())); @@ -305,7 +305,7 @@ private void updateChartData() { } // Get all entries for the defined time interval - tradeStatisticsByCurrency.forEach(tradeStatistics -> { + selectedTradeStatistics.forEach(tradeStatistics -> { for (long i = maxTicks; i > 0; --i) { Pair> pair = itemsPerInterval.get(i); if (tradeStatistics.getDate().after(pair.getKey())) { From ec7521628b5d9e5576fbafa0de80e0a78ced2864 Mon Sep 17 00:00:00 2001 From: chimp1984 Date: Fri, 8 Jan 2021 19:53:40 -0500 Subject: [PATCH 11/12] Only update selectedTradeStatistics if selected currency has changed or we got added new trade stats --- .../market/trades/TradesChartsViewModel.java | 40 ++++++++++--------- 1 file changed, 22 insertions(+), 18 deletions(-) diff --git a/desktop/src/main/java/bisq/desktop/main/market/trades/TradesChartsViewModel.java b/desktop/src/main/java/bisq/desktop/main/market/trades/TradesChartsViewModel.java index 904f3a00d2e..437e5cdc0c6 100644 --- a/desktop/src/main/java/bisq/desktop/main/market/trades/TradesChartsViewModel.java +++ b/desktop/src/main/java/bisq/desktop/main/market/trades/TradesChartsViewModel.java @@ -132,6 +132,7 @@ public enum TickUnit { this.navigation = navigation; setChangeListener = change -> { + updateSelectedTradeStatistics(getCurrencyCode()); updateChartData(); fillTradeCurrencies(); }; @@ -156,6 +157,7 @@ protected void activate() { fillTradeCurrenciesOnActiavetCalled = true; } buildUsdPricesPerDay(); + updateSelectedTradeStatistics(getCurrencyCode()); updateChartData(); syncPriceFeedCurrency(); setMarketPriceFeedCurrency(); @@ -172,25 +174,26 @@ protected void deactivate() { void onSetTradeCurrency(TradeCurrency tradeCurrency) { if (tradeCurrency != null) { - final String code = tradeCurrency.getCode(); + String code = tradeCurrency.getCode(); if (isEditEntry(code)) { navigation.navigateTo(MainView.class, SettingsView.class, PreferencesView.class); - } else { - boolean showAllEntry = isShowAllEntry(code); - showAllTradeCurrenciesProperty.set(showAllEntry); - if (!showAllEntry) { - selectedTradeCurrencyProperty.set(tradeCurrency); - } - preferences.setTradeChartsScreenCurrencyCode(code); + return; + } - updateChartData(); + boolean showAllEntry = isShowAllEntry(code); + showAllTradeCurrenciesProperty.set(showAllEntry); + if (showAllEntry) { + priceFeedService.setCurrencyCode(GlobalSettings.getDefaultTradeCurrency().getCode()); + } else { + selectedTradeCurrencyProperty.set(tradeCurrency); + priceFeedService.setCurrencyCode(code); - if (showAllEntry) - priceFeedService.setCurrencyCode(GlobalSettings.getDefaultTradeCurrency().getCode()); - else - priceFeedService.setCurrencyCode(code); } + preferences.setTradeChartsScreenCurrencyCode(code); + + updateSelectedTradeStatistics(getCurrencyCode()); + updateChartData(); } } @@ -288,11 +291,6 @@ private long getAveragePrice(List tradeStatisticsList) { } private void updateChartData() { - String currencyCode = getCurrencyCode(); - selectedTradeStatistics.setAll(tradeStatisticsManager.getObservableTradeStatisticsSet().stream() - .filter(e -> showAllTradeCurrenciesProperty.get() || e.getCurrency().equals(currencyCode)) - .collect(Collectors.toList())); - // Generate date range and create sets for all ticks itemsPerInterval = new HashMap<>(); Date time = new Date(); @@ -345,6 +343,12 @@ private void updateChartData() { .collect(Collectors.toList())); } + private void updateSelectedTradeStatistics(String currencyCode) { + selectedTradeStatistics.setAll(tradeStatisticsManager.getObservableTradeStatisticsSet().stream() + .filter(e -> showAllTradeCurrenciesProperty.get() || e.getCurrency().equals(currencyCode)) + .collect(Collectors.toList())); + } + @VisibleForTesting CandleData getCandleData(long tick, Set set, long averageUsdPrice) { long open = 0; From 4e444269af80ec4343c6285855b35087879be806 Mon Sep 17 00:00:00 2001 From: chimp1984 Date: Fri, 8 Jan 2021 19:57:39 -0500 Subject: [PATCH 12/12] Remove unnecessary ObservableList --- .../main/market/trades/TradesChartsViewModel.java | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/desktop/src/main/java/bisq/desktop/main/market/trades/TradesChartsViewModel.java b/desktop/src/main/java/bisq/desktop/main/market/trades/TradesChartsViewModel.java index 437e5cdc0c6..5227a4d00d2 100644 --- a/desktop/src/main/java/bisq/desktop/main/market/trades/TradesChartsViewModel.java +++ b/desktop/src/main/java/bisq/desktop/main/market/trades/TradesChartsViewModel.java @@ -372,11 +372,10 @@ CandleData getCandleData(long tick, Set set, long averageUsdPr Collections.sort(tradePrices); List list = new ArrayList<>(set); - ObservableList obsList = FXCollections.observableArrayList(list); - obsList.sort(Comparator.comparingLong(TradeStatistics3::getDateAsLong)); - if (obsList.size() > 0) { - open = obsList.get(0).getTradePrice().getValue(); - close = obsList.get(obsList.size() - 1).getTradePrice().getValue(); + list.sort(Comparator.comparingLong(TradeStatistics3::getDateAsLong)); + if (list.size() > 0) { + open = list.get(0).getTradePrice().getValue(); + close = list.get(list.size() - 1).getTradePrice().getValue(); } long averagePrice;