/** * Copyright 2012 multibit.org * * Licensed under the MIT license (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://opensource.org/licenses/mit-license.php * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.multibit.viewsystem.swing.view.panels; import com.google.bitcoin.core.Transaction; import com.xeiam.xchart.*; import org.multibit.controller.Controller; import org.multibit.controller.bitcoin.BitcoinController; import org.multibit.model.core.CoreModel; import org.multibit.utils.DateUtils; import org.multibit.utils.ImageLoader; import org.multibit.viewsystem.DisplayHint; import org.multibit.viewsystem.View; import org.multibit.viewsystem.Viewable; import org.multibit.viewsystem.swing.ColorAndFontConstants; import org.multibit.viewsystem.swing.MultiBitFrame; import org.multibit.viewsystem.swing.view.components.FontSizer; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.swing.*; import java.awt.*; import java.awt.event.ComponentEvent; import java.awt.event.ComponentListener; import java.math.BigInteger; import java.text.DateFormat; import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.*; /** * The Charts view. */ public class ChartsPanel extends JPanel implements Viewable, ComponentListener { private Logger log = LoggerFactory.getLogger(ChartsPanel.class); private static final long serialVersionUID = 191352212345998705L; private static final int NUMBER_OF_DAYS_TO_LOOK_BACK = 30; private static final double NUMBER_OF_SATOSHI_IN_ONE_BTC = 100000000; private static final int WIDTH_DELTA = 40; private static final int HEIGHT_DELTA = 20; private static final int MINIMUM_WIDTH = 800; private static final int MINIMUM_HEIGHT = 300; private static final String DATE_PATTERN = "dd-MMM"; private final Controller controller; private final BitcoinController bitcoinController; private JPanel mainPanel; private boolean generateRandomChart = false; /** * Creates a new {@link ChartsPanel}. */ public ChartsPanel(BitcoinController bitcoinController, MultiBitFrame mainFrame) { this.bitcoinController = bitcoinController; this.controller = this.bitcoinController; setLayout(new BorderLayout()); setBackground(ColorAndFontConstants.BACKGROUND_COLOR); setForeground(ColorAndFontConstants.TEXT_COLOR); setOpaque(true); applyComponentOrientation(ComponentOrientation.getOrientation(controller.getLocaliser().getLocale())); mainFrame.addComponentListener(this); addComponentListener(this); initUI(); } private void initUI() { mainPanel = new JPanel(); mainPanel.setLayout(new GridBagLayout()); mainPanel.setOpaque(true); mainPanel.setBackground(ColorAndFontConstants.BACKGROUND_COLOR); mainPanel.setForeground(ColorAndFontConstants.TEXT_COLOR); mainPanel.applyComponentOrientation(ComponentOrientation.getOrientation(controller.getLocaliser().getLocale())); GridBagConstraints constraints = new GridBagConstraints(); constraints.fill = GridBagConstraints.BOTH; constraints.gridx = 0; constraints.gridy = 0; constraints.gridwidth = 1; constraints.weightx = 1; constraints.weighty = 1; constraints.anchor = GridBagConstraints.CENTER; // Initially blank. JPanel chartPanel = new JPanel(); chartPanel.setOpaque(true); chartPanel.setBackground(ColorAndFontConstants.BACKGROUND_COLOR); chartPanel.setForeground(ColorAndFontConstants.TEXT_COLOR); mainPanel.add(chartPanel, constraints); JScrollPane mainScrollPane = new JScrollPane(mainPanel, JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED, JScrollPane.HORIZONTAL_SCROLLBAR_AS_NEEDED); mainScrollPane.setBorder(BorderFactory.createEmptyBorder()); mainScrollPane.getViewport().setBackground(ColorAndFontConstants.BACKGROUND_COLOR); mainScrollPane.getViewport().setForeground(ColorAndFontConstants.TEXT_COLOR); mainScrollPane.getViewport().setOpaque(true); mainScrollPane.applyComponentOrientation(ComponentOrientation.getOrientation(controller.getLocaliser().getLocale())); mainScrollPane.getHorizontalScrollBar().setUnitIncrement(CoreModel.SCROLL_INCREMENT); mainScrollPane.getVerticalScrollBar().setUnitIncrement(CoreModel.SCROLL_INCREMENT); add(mainScrollPane, BorderLayout.CENTER); } /** * Create a panel containing the chart to show. */ private JPanel createChartPanel() { try { setBackground(ColorAndFontConstants.BACKGROUND_COLOR); setForeground(ColorAndFontConstants.TEXT_COLOR); int chartWidth = Math.max(getWidth() - WIDTH_DELTA, MINIMUM_WIDTH); int chartHeight = Math.max(getHeight() - HEIGHT_DELTA, MINIMUM_HEIGHT); Chart chart = new Chart(chartWidth, chartHeight); Locale locale = controller.getLocaliser().getLocale(); chart.getStyleManager().setLocale(locale); // generates linear data Collection<Date> xData = new ArrayList<Date>(); Collection<Number> yData = new ArrayList<Number>(); // Get the last month's transaction data. Collection<ChartData> chartDataCollection = getChartData(); if (generateRandomChart) { DateFormat sdf = new SimpleDateFormat("dd.MM.yyyy"); Date date; for (int i = 1; i <= 10; i++) { try { date = sdf.parse(i + ".10.2008"); xData.add(date); } catch (ParseException e) { e.printStackTrace(); } yData.add(Math.random() * i); } } else { if (chartDataCollection == null || chartDataCollection.size() == 0) { log.debug("chartDataCollection is null or empty"); JPanel chartPanel = new JPanel(); chartPanel.setBackground(ColorAndFontConstants.BACKGROUND_COLOR); chartPanel.setForeground(ColorAndFontConstants.TEXT_COLOR); chartPanel.setOpaque(true); return chartPanel; } else { for (ChartData chartData : chartDataCollection) { if (chartData != null && chartData.getDate() != null && chartData.getValue() != null) { xData.add(chartData.getDate()); yData.add(chartData.getValue().doubleValue() / NUMBER_OF_SATOSHI_IN_ONE_BTC); } } } } // Customize Chart. String xAxisLabel = controller.getLocaliser().getString("walletData.dateText"); String currencyUnitSuffix = " (" + controller.getLocaliser().getString("sendBitcoinPanel.amountUnitLabel") + ")"; String balanceLabel = controller.getLocaliser().getString("multiBitFrame.balanceLabel") + currencyUnitSuffix; String unitOfTime = controller.getLocaliser().getString("chartsPanelTitle.days"); String chartTitle = controller.getLocaliser().getString("chartsPanelTitle.text", new Object[] { NUMBER_OF_DAYS_TO_LOOK_BACK, unitOfTime }) + currencyUnitSuffix; chart.getStyleManager().setPlotGridLinesVisible(false); chart.getStyleManager().setXAxisTicksVisible(true); chart.getStyleManager().setLegendVisible(false); chart.getStyleManager().setChartBackgroundColor(ColorAndFontConstants.BACKGROUND_COLOR); chart.getStyleManager().setChartFontColor(ColorAndFontConstants.TEXT_COLOR); chart.getStyleManager().setAxisTickLabelsColor(ColorAndFontConstants.TEXT_COLOR); chart.getStyleManager().setAxisTickMarksColor(ColorAndFontConstants.TEXT_COLOR); chart.getStyleManager().setChartTitleFont(FontSizer.INSTANCE.getAdjustedDefaultFontWithDelta(2)); chart.getStyleManager().setAxisTitleFont(FontSizer.INSTANCE.getAdjustedDefaultFont()); chart.getStyleManager().setAxisTickLabelsFont(FontSizer.INSTANCE.getAdjustedDefaultFontWithDelta(-2)); chart.getStyleManager().setDatePattern(DATE_PATTERN); chart.setChartTitle(chartTitle); chart.setXAxisTitle(xAxisLabel); com.xeiam.xchart.Series series = chart.addDateSeries(balanceLabel, xData, yData); series.setLineColor(SeriesColor.BLUE); series.setMarkerColor(SeriesColor.BLUE); series.setMarker(SeriesMarker.CIRCLE); series.setLineStyle(SeriesLineStyle.SOLID); XChartPanel chartPanelToReturn = new XChartPanel(chart); chartPanelToReturn.setSaveAsString(controller.getLocaliser().getString("chartsPanelSaveAs")); chartPanelToReturn.setLocale(locale); chartPanelToReturn.setMinimumSize(new Dimension(chartWidth, chartHeight)); return chartPanelToReturn; } catch (Exception e) { e.printStackTrace(); JPanel chartPanel = new JPanel(); chartPanel.setBackground(ColorAndFontConstants.BACKGROUND_COLOR); chartPanel.setForeground(ColorAndFontConstants.TEXT_COLOR); chartPanel.setOpaque(true); return chartPanel; } } /** * Update the chart panel (The active wallet may have changed). */ private void updateChart() { // Clear the main panel. mainPanel.removeAll(); // Recreate the chart data and 'draw' it. GridBagConstraints constraints = new GridBagConstraints(); constraints.fill = GridBagConstraints.BOTH; constraints.gridx = 0; constraints.gridy = 0; constraints.gridwidth = 1; constraints.weightx = 1; constraints.weighty = 1; constraints.anchor = GridBagConstraints.CENTER; // Recreate chart panel. JPanel chartPanel = createChartPanel(); chartPanel.setOpaque(true); chartPanel.setBackground(ColorAndFontConstants.BACKGROUND_COLOR); chartPanel.setForeground(ColorAndFontConstants.TEXT_COLOR); mainPanel.add(chartPanel, constraints); } /** * Get the transaction data for the chart */ private Collection<ChartData> getChartData() { if (controller.getModel() == null || this.bitcoinController.getModel().getActiveWallet() == null) { return new ArrayList<ChartData>(); } ArrayList<Transaction> allTransactions = new ArrayList<Transaction>(this.bitcoinController.getModel().getActiveWallet().getTransactions(false)); // Order by date. Collections.sort(allTransactions, new Comparator<Transaction>() { @Override public int compare(Transaction t1, Transaction t2) { Date date1 = t1.getUpdateTime(); Date date2 = t2.getUpdateTime(); if (date1 == null) { if (date2 == null) { return 0; } else { return -1; } } else { if (date2 == null) { return 1; } else { return date1.compareTo(date2); } } } }); // Work out balance as running total and filter to just last // NUMBER_OF_DAYS_TO_LOOK_BACKs data. BigInteger balance = BigInteger.ZERO; // The previous datums balance. BigInteger previousBalance = BigInteger.ZERO; // The previous datums timepoint. Date previousDate = null; long pastInMillis = DateUtils.nowUtc().plusDays(-1 * NUMBER_OF_DAYS_TO_LOOK_BACK).getMillis(); // Create ChartData collection. Collection<ChartData> chartData = new ArrayList<ChartData>(); try { boolean leftEdgeDataPointAdded = false; if (allTransactions == null || allTransactions.size() == 0) { // At beginning of time window balance was zero chartData.add(new ChartData(new Date(pastInMillis), BigInteger.ZERO)); } else { for (Transaction loop : allTransactions) { balance = balance.add(loop.getValue(this.bitcoinController.getModel().getActiveWallet())); Date loopUpdateTime = loop.getUpdateTime(); if (loopUpdateTime != null) { long loopTimeInMillis = loopUpdateTime.getTime(); if (loopTimeInMillis > pastInMillis) { if (!leftEdgeDataPointAdded) { // If the previous transaction was BEFORE the // NUMBER_OF_DAYS_TO_LOOK_BACK cutoff, include a // datapoint at the beginning of the timewindow // with the balance // at that time. if ((previousDate != null) && (previousDate.getTime() <= pastInMillis)) { // The balance was non-zero. chartData.add(new ChartData(new Date(pastInMillis), previousBalance)); } else { // At beginning of time window balance was // zero chartData.add(new ChartData(new Date(pastInMillis), BigInteger.ZERO)); } leftEdgeDataPointAdded = true; } // Include this transaction as it is in the last // NUMBER_OF_DAYS_TO_LOOK_BACK days. chartData.add(new ChartData(loop.getUpdateTime(), previousBalance)); chartData.add(new ChartData(loop.getUpdateTime(), balance)); } previousBalance = balance; previousDate = loop.getUpdateTime(); } } } // If all the datapoints are before the left hand edge, ensure the balance is also added at the left hand edge. if (!leftEdgeDataPointAdded) { chartData.add(new ChartData(new Date(pastInMillis), balance)); } // Add in the balance at the end of the time window. chartData.add(new ChartData(new Date(DateUtils.nowUtc().getMillis()), balance)); // log.debug("Last transaction date = " + previousDate + ", chart balance = " + balance + ", wallet balance = " + controller.getModel().getActiveWallet().getBalance()); } catch (com.google.bitcoin.core.ScriptException e1) { e1.printStackTrace(); } return chartData; } @Override /** * Release any resources used when user navigates away from this view. */ public void navigateAwayFromView() { } @Override public void displayView(DisplayHint displayHint) { updateChart(); } @Override public Icon getViewIcon() { return ImageLoader.createImageIcon(ImageLoader.CHART_LINE_ICON_FILE); } @Override public String getViewTitle() { return controller.getLocaliser().getString("chartsPanelAction.text"); } @Override public String getViewTooltip() { return controller.getLocaliser().getString("chartsPanelAction.tooltip"); } @Override public View getViewId() { return View.CHARTS_VIEW; } static class ChartData { private Date date; private BigInteger value; public ChartData(Date date, BigInteger value) { this.date = date; this.value = value; } public Date getDate() { return date; } public BigInteger getValue() { return value; } @Override public String toString() { return "ChartData [date=" + date + ", value=" + value + "]"; } } @Override public void componentHidden(ComponentEvent arg0) { } @Override public void componentMoved(ComponentEvent arg0) { } @Override public void componentResized(ComponentEvent arg0) { updateChart(); } @Override public void componentShown(ComponentEvent arg0) { } }