org.yccheok.jstock.gui.charting.ChartJDialog.java Source code

Java tutorial

Introduction

Here is the source code for org.yccheok.jstock.gui.charting.ChartJDialog.java

Source

/*
 * JStock - Free Stock Market Software
 * Copyright (C) 2012 Yan Cheng CHEOK <yccheok@yahoo.com>
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * This program 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 General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License along
 * with this program; if not, write to the Free Software Foundation, Inc.,
 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
 */

package org.yccheok.jstock.gui.charting;

import java.awt.Dimension;
import java.awt.Font;
import java.awt.Toolkit;
import java.awt.event.ActionEvent;
import java.awt.event.ComponentEvent;
import java.awt.event.WindowEvent;
import java.awt.event.WindowListener;
import java.io.File;
import org.yccheok.jstock.engine.*;

import java.util.*;
import java.text.DecimalFormat;
import java.text.SimpleDateFormat;

import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
import javax.swing.AbstractAction;
import javax.swing.Action;
import javax.swing.ComboBoxModel;
import javax.swing.JComponent;
import javax.swing.JLabel;
import javax.swing.JMenu;
import javax.swing.JMenuItem;
import javax.swing.JOptionPane;
import javax.swing.KeyStroke;
import javax.swing.SwingUtilities;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.jfree.chart.ChartPanel;
import org.jfree.chart.JFreeChart;
import org.jfree.chart.axis.DateAxis;
import org.jfree.chart.axis.NumberAxis;
import org.jfree.chart.axis.ValueAxis;
import org.jfree.chart.event.ChartChangeEvent;
import org.jfree.chart.event.ChartChangeEventType;
import org.jfree.chart.event.ChartChangeListener;
import org.jfree.chart.labels.StandardXYToolTipGenerator;
import org.jfree.chart.plot.CombinedDomainXYPlot;
import org.jfree.chart.plot.Plot;
import org.jfree.chart.plot.XYPlot;
import org.jfree.chart.renderer.xy.CandlestickRenderer;
import org.jfree.chart.renderer.xy.StandardXYItemRenderer;
import org.jfree.chart.renderer.xy.XYBarRenderer;
import org.jfree.chart.renderer.xy.XYItemRenderer;
import org.jfree.chart.renderer.xy.XYLineAndShapeRenderer;
import org.jfree.data.Range;
import org.jfree.data.time.Day;
import org.jfree.data.time.TimeSeries;
import org.jfree.data.time.TimeSeriesCollection;
import org.jfree.data.time.TimeSeriesDataItem;
import org.jfree.data.xy.*;
import org.yccheok.jstock.charting.ChartData;
import org.yccheok.jstock.charting.MACD;
import org.yccheok.jstock.charting.TechnicalAnalysis;
import org.yccheok.jstock.file.Statements;
import org.yccheok.jstock.gui.JStockOptions;
import org.yccheok.jstock.gui.JStock;
import org.yccheok.jstock.gui.charting.ChartJDialog.TAEx;
import org.yccheok.jstock.internationalization.GUIBundle;
import org.yccheok.jstock.internationalization.MessagesBundle;
import org.yccheok.jstock.network.Utils;

/**
 *
 * @author  yccheok
 */
public class ChartJDialog extends javax.swing.JFrame implements WindowListener {
    public enum TA {
        SMA, EMA, MACD, RSI, MFI, CCI,
    }

    public enum Type {
        PriceVolume, Candlestick
    }

    public enum Interval {
        Daily, Weekly, Monthly
    }

    public enum Zoom {
        Days7, Month1, Months3, Months6, Year1, Year5, Year10, All
    }

    private Interval getCurrentInterval() {
        return this.currentInverval;
    }

    private Type getCurrentType() {
        return this.currentType;
    }

    /** Creates new form ChartJDialog */
    public ChartJDialog(java.awt.Frame parent, String title, boolean modalNotUsed,
            StockHistoryServer stockHistoryServer) {
        super(title);

        this.parent = parent;
        this.parent.addWindowListener(this);

        this.setIconImage(parent.getIconImage());

        initComponents();
        initKeyBindings();

        // Must initialized first before any other operations. Our objective
        // is to show this chart as fast as possible. Hence, we will pass in
        // null first, till we sure what are we going to create.
        this.chartPanel = new ChartPanel(null, true, true, true, true, true);
        this.stockHistoryServer = stockHistoryServer;

        final ChartJDialogOptions chartJDialogOptions = JStock.instance().getChartJDialogOptions();
        this.changeInterval(chartJDialogOptions.getInterval());

        // Yellow box and chart resizing (#2969416)
        //
        // paradoxoff :
        // If the available size for a ChartPanel exceeds the dimensions defined
        // by the maximumDrawWidth and maximumDrawHeight attributes, the
        // ChartPanel will draw the chart in a Rectangle defined by the maximum
        // sizes and then scale it to fill the available size.
        // All you need to do is to avoid that scaling by using sufficiently
        // large values for the maximumDrawWidth and maximumDrawHeight, e. g.
        // by using the Dimension returned from
        // Toolkit.getDefaultToolkit().getScreenSize()
        // http://www.jfree.org/phpBB2/viewtopic.php?f=3&t=30059
        final Dimension dimension = Toolkit.getDefaultToolkit().getScreenSize();
        this.chartPanel.setMaximumDrawWidth((int) Math.round(dimension.getWidth()));
        this.chartPanel.setMaximumDrawHeight((int) Math.round(dimension.getHeight()));

        // Make chartPanel able to receive key event.
        // So that we may use arrow key to move around yellow information box.
        this.chartPanel.setFocusable(true);
        this.chartPanel.requestFocus();

        final org.jdesktop.jxlayer.JXLayer<ChartPanel> layer = new org.jdesktop.jxlayer.JXLayer<ChartPanel>(
                this.chartPanel);
        this.chartLayerUI = new ChartLayerUI<ChartPanel>(this);
        layer.setUI(this.chartLayerUI);

        // Call after JXLayer has been initialized. changeType assumes JXLayer 
        // is ready.
        this.changeType(chartJDialogOptions.getType());

        getContentPane().add(layer, java.awt.BorderLayout.CENTER);

        // Handle resize.
        this.addComponentListener(new java.awt.event.ComponentAdapter() {
            @Override
            public void componentResized(ComponentEvent e) {
                ChartJDialog.this.chartLayerUI.updateTraceInfos();
            }
        });

        // Update GUI.
        if (JStock.instance().getJStockOptions().getChartTheme() == JStockOptions.ChartTheme.Light) {
            this.jRadioButtonMenuItem3.setSelected(true);
        } else {
            this.jRadioButtonMenuItem4.setSelected(true);
        }
    }

    public void windowActivated(WindowEvent e) {
        // this only works becuase AutoRequestFocus is false, so this stays on
        // top, but looses focus 
        this.toFront();
    }

    @Override
    public void windowDeactivated(WindowEvent e) {
        // JFrame is set to AlwaysOnTop = true at design time. So this is only
        // useful on first deactivation. After that it is meaningless. But without
        // initial AlwaysOnTop, it would not receive focus because autoRequestFocus
        // is false.
        this.setAlwaysOnTop(false);
    }

    @Override
    public void windowIconified(WindowEvent e) {
        // when main app goes away, this child window should also go away 
        this.setVisible(false);
    }

    @Override
    public void windowDeiconified(WindowEvent e) {
        // when main app comes back, this child window should also come back
        this.setVisible(true);
    }

    @Override
    public void windowClosed(WindowEvent e) {
    }

    @Override
    public void windowClosing(WindowEvent e) {
    }

    @Override
    public void windowOpened(WindowEvent e) {
    }

    /**
     * Build menu items for TA.
     */
    private void buildTAMenuItems() {
        final int[] days = { 14, 28, 50, 100, 200 };
        final MACD.Period[] macd_periods = { MACD.Period.newInstance(12, 26, 9) };

        // day_keys, week_keys and month_keys should be having same length as
        // days.        
        final String[] day_keys = { "ChartJDialog_14Days", "ChartJDialog_28Days", "ChartJDialog_50Days",
                "ChartJDialog_100Days", "ChartJDialog_200Days" };
        final String[] week_keys = { "ChartJDialog_14Weeks", "ChartJDialog_28Weeks", "ChartJDialog_50Weeks",
                "ChartJDialog_100Weeks", "ChartJDialog_200Weeks" };
        final String[] month_keys = { "ChartJDialog_14Months", "ChartJDialog_28Months", "ChartJDialog_50Months",
                "ChartJDialog_100Months", "ChartJDialog_200Months" };
        assert (days.length == day_keys.length);
        assert (days.length == week_keys.length);
        assert (days.length == week_keys.length);
        // macd_day_keys, macd_week_keys and macd_month_keys should be having 
        // same length as macd_periods.
        final String[] macd_day_keys = { "ChartJDialog_12_26_9Days" };
        final String[] macd_week_keys = { "ChartJDialog_12_26_9Weeks" };
        final String[] macd_month_keys = { "ChartJDialog_12_26_9Months" };
        assert (macd_periods.length == macd_day_keys.length);
        assert (macd_periods.length == macd_week_keys.length);
        assert (macd_periods.length == macd_month_keys.length);

        String[] keys = null;
        String[] macd_keys = null;
        if (this.getCurrentInterval() == Interval.Daily) {
            keys = day_keys;
            macd_keys = macd_day_keys;
        } else if (this.getCurrentInterval() == Interval.Weekly) {
            keys = week_keys;
            macd_keys = macd_week_keys;
        } else if (this.getCurrentInterval() == Interval.Monthly) {
            keys = month_keys;
            macd_keys = macd_month_keys;
        } else {
            assert (false);
        }
        final TA[] tas = TA.values();
        final String[] ta_keys = { "ChartJDialog_SMA", "ChartJDialog_EMA", "ChartJDialog_MACD", "ChartJDialog_RSI",
                "ChartJDialog_MFI", "ChartJDialog_CCI" };
        final String[] ta_tip_keys = { "ChartJDialog_SimpleMovingAverage", "ChartJDialog_ExponentialMovingAverage",
                "ChartJDialog_MovingAverageConvergenceDivergence", "ChartJDialog_RelativeStrengthIndex",
                "ChartJDialog_MoneyFlowIndex", "ChartJDialog_CommodityChannelIndex" };
        final String[] custom_message_keys = { "info_message_please_enter_number_of_days_for_SMA",
                "info_message_please_enter_number_of_days_for_EMA", "dummy",
                "info_message_please_enter_number_of_days_for_RSI",
                "info_message_please_enter_number_of_days_for_MFI",
                "info_message_please_enter_number_of_days_for_CCI" };
        final Map<TA, Set<Object>> m = new EnumMap<TA, Set<Object>>(TA.class);
        final int taExSize = JStock.instance().getChartJDialogOptions().getTAExSize();
        for (int i = 0; i < taExSize; i++) {
            final TAEx taEx = JStock.instance().getChartJDialogOptions().getTAEx(i);
            if (m.containsKey(taEx.getTA()) == false) {
                m.put(taEx.getTA(), new HashSet<Object>());
            }
            m.get(taEx.getTA()).add(taEx.getParameter());
        }

        for (int i = 0, length = tas.length; i < length; i++) {
            final TA ta = tas[i];
            javax.swing.JMenu menu = new javax.swing.JMenu();
            menu.setText(GUIBundle.getString(ta_keys[i])); // NOI18N
            menu.setToolTipText(GUIBundle.getString(ta_tip_keys[i])); // NOI18N

            if (ta == TA.MACD) {
                for (int j = 0, length2 = macd_periods.length; j < length2; j++) {
                    final int _j = j;
                    final javax.swing.JCheckBoxMenuItem item = new javax.swing.JCheckBoxMenuItem();
                    item.setText(GUIBundle.getString(macd_keys[j]));
                    if (m.containsKey(ta)) {
                        if (m.get(ta).contains(macd_periods[_j])) {
                            item.setSelected(true);
                        }
                    }
                    item.addActionListener(new java.awt.event.ActionListener() {
                        @Override
                        public void actionPerformed(java.awt.event.ActionEvent evt) {
                            if (ta == TA.MACD) {
                                updateMACD(macd_periods[_j], item.isSelected());
                            }
                        }
                    });
                    menu.add(item);
                }
            } else {
                for (int j = 0, length2 = days.length; j < length2; j++) {
                    final int _j = j;
                    final javax.swing.JCheckBoxMenuItem item = new javax.swing.JCheckBoxMenuItem();
                    item.setText(GUIBundle.getString(keys[j])); // NOI18N
                    if (m.containsKey(ta)) {
                        if (m.get(ta).contains(days[_j])) {
                            item.setSelected(true);
                        }
                    }
                    item.addActionListener(new java.awt.event.ActionListener() {
                        @Override
                        public void actionPerformed(java.awt.event.ActionEvent evt) {
                            if (ta == TA.SMA) {
                                updateSMA(days[_j], item.isSelected());
                            } else if (ta == TA.EMA) {
                                updateEMA(days[_j], item.isSelected());
                            } else if (ta == TA.MFI) {
                                updateMFI(days[_j], item.isSelected());
                            } else if (ta == TA.RSI) {
                                updateRSI(days[_j], item.isSelected());
                            } else if (ta == TA.CCI) {
                                updateCCI(days[_j], item.isSelected());
                            }
                        }
                    });
                    menu.add(item);
                } // for
            } // if (ta == TA.MACD)

            menu.add(new javax.swing.JSeparator());
            javax.swing.JMenuItem item = new javax.swing.JMenuItem();
            item.setText(GUIBundle.getString("ChartJDialog_Custom...")); // NOI18N
            final int _i = i;
            item.addActionListener(new java.awt.event.ActionListener() {
                @Override
                public void actionPerformed(java.awt.event.ActionEvent evt) {
                    if (ta == TA.MACD) {
                        showMACDCustomDialog();
                    } else {
                        do {
                            final String days_string = JOptionPane.showInputDialog(ChartJDialog.this,
                                    MessagesBundle.getString(custom_message_keys[_i]));
                            if (days_string == null) {
                                return;
                            }
                            try {
                                final int days = Integer.parseInt(days_string);
                                if (days <= 0) {
                                    JOptionPane.showMessageDialog(ChartJDialog.this,
                                            MessagesBundle.getString("info_message_number_of_days_required"),
                                            MessagesBundle.getString("info_title_number_of_days_required"),
                                            JOptionPane.WARNING_MESSAGE);
                                    continue;
                                }
                                ChartJDialog.this.updateTA(ta, days, true);
                                return;
                            } catch (java.lang.NumberFormatException exp) {
                                log.error(null, exp);
                                JOptionPane.showMessageDialog(ChartJDialog.this,
                                        MessagesBundle.getString("info_message_number_of_days_required"),
                                        MessagesBundle.getString("info_title_number_of_days_required"),
                                        JOptionPane.WARNING_MESSAGE);
                                continue;
                            }
                        } while (true);
                    }
                }
            });
            menu.add(item);

            // TEMP DISABLE MACD
            // !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
            // !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
            // !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
            if (ta != TA.MACD) {
                this.jMenu2.add(menu);
            }
        } // for
        this.jMenu2.add(new javax.swing.JSeparator());
        javax.swing.JMenuItem item = new javax.swing.JMenuItem();
        item.setText(GUIBundle.getString("ChartJDialog_ClearAll")); // NOI18N
        item.addActionListener(new java.awt.event.ActionListener() {
            @Override
            public void actionPerformed(java.awt.event.ActionEvent evt) {
                ChartJDialog.this.clearAll();
            }
        });
        this.jMenu2.add(item);
    }

    private void updateTA(TA ta, int days, boolean show) {
        if (ta == TA.SMA) {
            updateSMA(days, show);
        } else if (ta == TA.EMA) {
            updateEMA(days, show);
        } else if (ta == TA.MFI) {
            updateMFI(days, show);
        } else if (ta == TA.RSI) {
            updateRSI(days, show);
        } else if (ta == TA.CCI) {
            updateCCI(days, show);
        }
    }

    /**
     * Restores previous chart dialog options.
     */
    private void loadChartJDialogOptions() {
        final ChartJDialogOptions chartJDialogOptions = JStock.instance().getChartJDialogOptions();
        /* Zoom in to which days? */
        ChartJDialog.this.zoom(chartJDialogOptions.getZoom());
        /* Restore TA information. */
        final int TAExSize = chartJDialogOptions.getTAExSize();
        for (int i = 0; i < TAExSize; i++) {
            final TAEx taEx = chartJDialogOptions.getTAEx(i);
            final TA ta = taEx.getTA();
            if (ta == TA.MACD) {
                final MACD.Period period = (MACD.Period) taEx.getParameter();
                ChartJDialog.this.updateMACD(period, true);
            } else {
                final Integer day = (Integer) taEx.getParameter();
                ChartJDialog.this.updateTA(ta, day, true);
            }
        }
    }

    public List<ChartData> getChartDatas() {
        return Collections.unmodifiableList(chartDatas);
    }

    public ChartPanel getChartPanel() {
        return this.chartPanel;
    }

    /** This method is called from within the constructor to
     * initialize the form.
     * WARNING: Do NOT modify this code. The content of this method is
     * always regenerated by the Form Editor.
     */
    // <editor-fold defaultstate="collapsed" desc="Generated Code">//GEN-BEGIN:initComponents
    private void initComponents() {

        buttonGroup1 = new javax.swing.ButtonGroup();
        buttonGroup2 = new javax.swing.ButtonGroup();
        jPanel4 = new javax.swing.JPanel();
        jPanel2 = new javax.swing.JPanel();
        jPanel1 = new javax.swing.JPanel();
        jLabel1 = new javax.swing.JLabel();
        jLabel3 = new javax.swing.JLabel();
        jPanel3 = new javax.swing.JPanel();
        jLabel2 = new javax.swing.JLabel();
        jLabel4 = new javax.swing.JLabel();
        jPanel5 = new javax.swing.JPanel();
        jComboBox1 = new javax.swing.JComboBox();
        jPanel6 = new javax.swing.JPanel();
        jLabel9 = new org.yccheok.jstock.gui.charting.HyperlinkLikedJLabel();
        jLabel10 = new org.yccheok.jstock.gui.charting.HyperlinkLikedJLabel();
        jLabel11 = new org.yccheok.jstock.gui.charting.HyperlinkLikedJLabel();
        jLabel12 = new org.yccheok.jstock.gui.charting.HyperlinkLikedJLabel();
        jLabel13 = new org.yccheok.jstock.gui.charting.HyperlinkLikedJLabel();
        jLabel5 = new org.yccheok.jstock.gui.charting.HyperlinkLikedJLabel();
        jLabel6 = new org.yccheok.jstock.gui.charting.HyperlinkLikedJLabel();
        jLabel14 = new org.yccheok.jstock.gui.charting.HyperlinkLikedJLabel();
        jMenuBar1 = new javax.swing.JMenuBar();
        jMenu1 = new javax.swing.JMenu();
        jMenuItem1 = new javax.swing.JMenuItem();
        jMenu2 = new javax.swing.JMenu();
        jMenu3 = new javax.swing.JMenu();
        jRadioButtonMenuItem1 = new javax.swing.JRadioButtonMenuItem();
        jRadioButtonMenuItem2 = new javax.swing.JRadioButtonMenuItem();
        jMenu4 = new javax.swing.JMenu();
        jRadioButtonMenuItem3 = new javax.swing.JRadioButtonMenuItem();
        jRadioButtonMenuItem4 = new javax.swing.JRadioButtonMenuItem();
        jMenu8 = new javax.swing.JMenu();
        jMenuItem7 = new javax.swing.JMenuItem();

        setDefaultCloseOperation(javax.swing.WindowConstants.DO_NOTHING_ON_CLOSE);
        setAlwaysOnTop(true);
        setAutoRequestFocus(false);
        addWindowListener(new java.awt.event.WindowAdapter() {
            public void windowClosing(java.awt.event.WindowEvent evt) {
                formWindowClosing(evt);
            }
        });
        getContentPane().setLayout(new java.awt.BorderLayout(5, 5));

        jPanel4.setBorder(javax.swing.BorderFactory.createEmptyBorder(5, 5, 2, 5));
        jPanel4.setLayout(new java.awt.BorderLayout(5, 5));

        jPanel2.setBorder(javax.swing.BorderFactory.createEmptyBorder(0, 0, 0, 160));
        jPanel2.setPreferredSize(new java.awt.Dimension(120, 33));

        jPanel1.setLayout(new java.awt.GridLayout(2, 1, 0, 5));

        java.util.ResourceBundle bundle = java.util.ResourceBundle.getBundle("org/yccheok/jstock/data/gui"); // NOI18N
        jLabel1.setText(bundle.getString("ChartJDialog_High")); // NOI18N
        jPanel1.add(jLabel1);

        jLabel3.setText(bundle.getString("ChartJDialog_Low")); // NOI18N
        jPanel1.add(jLabel3);

        jPanel3.setLayout(new java.awt.GridLayout(2, 1, 0, 5));

        jLabel2.setFont(jLabel2.getFont().deriveFont(jLabel2.getFont().getStyle() | java.awt.Font.BOLD));
        jLabel2.setHorizontalAlignment(javax.swing.SwingConstants.LEFT);
        jLabel2.setForeground(JStockOptions.DEFAULT_HIGHER_NUMERICAL_VALUE_FOREGROUND_COLOR);
        jPanel3.add(jLabel2);

        jLabel4.setFont(jLabel4.getFont().deriveFont(jLabel4.getFont().getStyle() | java.awt.Font.BOLD));
        jLabel4.setHorizontalAlignment(javax.swing.SwingConstants.LEFT);
        jLabel4.setForeground(JStockOptions.DEFAULT_LOWER_NUMERICAL_VALUE_FOREGROUND_COLOR);
        jPanel3.add(jLabel4);

        javax.swing.GroupLayout jPanel2Layout = new javax.swing.GroupLayout(jPanel2);
        jPanel2.setLayout(jPanel2Layout);
        jPanel2Layout.setHorizontalGroup(jPanel2Layout
                .createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
                .addGroup(jPanel2Layout.createSequentialGroup()
                        .addComponent(jPanel1, javax.swing.GroupLayout.PREFERRED_SIZE,
                                javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)
                        .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED,
                                javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)
                        .addComponent(jPanel3, javax.swing.GroupLayout.PREFERRED_SIZE, 71,
                                javax.swing.GroupLayout.PREFERRED_SIZE)
                        .addContainerGap()));
        jPanel2Layout.setVerticalGroup(jPanel2Layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
                .addGroup(jPanel2Layout.createSequentialGroup()
                        .addGroup(jPanel2Layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
                                .addComponent(jPanel1, javax.swing.GroupLayout.PREFERRED_SIZE, 34,
                                        javax.swing.GroupLayout.PREFERRED_SIZE)
                                .addComponent(jPanel3, javax.swing.GroupLayout.PREFERRED_SIZE, 34,
                                        javax.swing.GroupLayout.PREFERRED_SIZE))
                        .addContainerGap(javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)));

        jPanel4.add(jPanel2, java.awt.BorderLayout.WEST);

        jComboBox1.setModel(getComboBoxModel());
        jComboBox1.addActionListener(new java.awt.event.ActionListener() {
            public void actionPerformed(java.awt.event.ActionEvent evt) {
                jComboBox1ActionPerformed(evt);
            }
        });
        jPanel5.add(jComboBox1);

        jPanel4.add(jPanel5, java.awt.BorderLayout.EAST);

        jPanel6.setLayout(new java.awt.FlowLayout(java.awt.FlowLayout.CENTER, 10, 10));

        jLabel9.setText(bundle.getString("ChartJDialog_7Days")); // NOI18N
        jLabel9.addMouseListener(new java.awt.event.MouseAdapter() {
            public void mouseClicked(java.awt.event.MouseEvent evt) {
                jLabel9MouseClicked(evt);
            }
        });
        jPanel6.add(jLabel9);

        jLabel10.setText(bundle.getString("ChartJDialog_1Month")); // NOI18N
        jLabel10.addMouseListener(new java.awt.event.MouseAdapter() {
            public void mouseClicked(java.awt.event.MouseEvent evt) {
                jLabel10MouseClicked(evt);
            }
        });
        jPanel6.add(jLabel10);

        jLabel11.setText(bundle.getString("ChartJDialog_3Months")); // NOI18N
        jLabel11.addMouseListener(new java.awt.event.MouseAdapter() {
            public void mouseClicked(java.awt.event.MouseEvent evt) {
                jLabel11MouseClicked(evt);
            }
        });
        jPanel6.add(jLabel11);

        jLabel12.setText(bundle.getString("ChartJDialog_6Months")); // NOI18N
        jLabel12.addMouseListener(new java.awt.event.MouseAdapter() {
            public void mouseClicked(java.awt.event.MouseEvent evt) {
                jLabel12MouseClicked(evt);
            }
        });
        jPanel6.add(jLabel12);

        jLabel13.setText(bundle.getString("ChartJDialog_1Year")); // NOI18N
        jLabel13.addMouseListener(new java.awt.event.MouseAdapter() {
            public void mouseClicked(java.awt.event.MouseEvent evt) {
                jLabel13MouseClicked(evt);
            }
        });
        jPanel6.add(jLabel13);

        jLabel5.setText(bundle.getString("ChartJDialog_5Years")); // NOI18N
        jLabel5.addMouseListener(new java.awt.event.MouseAdapter() {
            public void mouseClicked(java.awt.event.MouseEvent evt) {
                jLabel5MouseClicked(evt);
            }
        });
        jPanel6.add(jLabel5);

        jLabel6.setText(bundle.getString("ChartJDialog_10Years")); // NOI18N
        jLabel6.addMouseListener(new java.awt.event.MouseAdapter() {
            public void mouseClicked(java.awt.event.MouseEvent evt) {
                jLabel6MouseClicked(evt);
            }
        });
        jPanel6.add(jLabel6);

        jLabel14.setText(bundle.getString("ChartJDialog_All")); // NOI18N
        jLabel14.addMouseListener(new java.awt.event.MouseAdapter() {
            public void mouseClicked(java.awt.event.MouseEvent evt) {
                jLabel14MouseClicked(evt);
            }
        });
        jPanel6.add(jLabel14);

        jPanel4.add(jPanel6, java.awt.BorderLayout.CENTER);

        getContentPane().add(jPanel4, java.awt.BorderLayout.SOUTH);

        jMenu1.setText(bundle.getString("ChartJDialog_File")); // NOI18N

        jMenuItem1.setIcon(new javax.swing.ImageIcon(getClass().getResource("/images/16x16/filesave.png"))); // NOI18N
        jMenuItem1.setText(bundle.getString("ChartJDialog_SaveAs...")); // NOI18N
        jMenuItem1.addActionListener(new java.awt.event.ActionListener() {
            public void actionPerformed(java.awt.event.ActionEvent evt) {
                jMenuItem1ActionPerformed(evt);
            }
        });
        jMenu1.add(jMenuItem1);

        jMenuBar1.add(jMenu1);

        jMenu2.setText(bundle.getString("ChartJDialog_TechnicalAnalysis")); // NOI18N
        jMenu2.addMenuListener(new javax.swing.event.MenuListener() {
            public void menuCanceled(javax.swing.event.MenuEvent evt) {
            }

            public void menuDeselected(javax.swing.event.MenuEvent evt) {
            }

            public void menuSelected(javax.swing.event.MenuEvent evt) {
                jMenu2MenuSelected(evt);
            }
        });
        jMenuBar1.add(jMenu2);

        jMenu3.setText(bundle.getString("ChartJDialog_Type")); // NOI18N

        buttonGroup1.add(jRadioButtonMenuItem1);
        jRadioButtonMenuItem1.setSelected(true);
        jRadioButtonMenuItem1.setText(bundle.getString("ChartJDialog_PriceVolume")); // NOI18N
        jRadioButtonMenuItem1.addActionListener(new java.awt.event.ActionListener() {
            public void actionPerformed(java.awt.event.ActionEvent evt) {
                jRadioButtonMenuItem1ActionPerformed(evt);
            }
        });
        jMenu3.add(jRadioButtonMenuItem1);

        buttonGroup1.add(jRadioButtonMenuItem2);
        jRadioButtonMenuItem2.setText(bundle.getString("ChartJDialog_Candlestick")); // NOI18N
        jRadioButtonMenuItem2.addActionListener(new java.awt.event.ActionListener() {
            public void actionPerformed(java.awt.event.ActionEvent evt) {
                jRadioButtonMenuItem2ActionPerformed(evt);
            }
        });
        jMenu3.add(jRadioButtonMenuItem2);

        jMenuBar1.add(jMenu3);

        jMenu4.setText(bundle.getString("ChartJDialog_Theme")); // NOI18N

        buttonGroup2.add(jRadioButtonMenuItem3);
        jRadioButtonMenuItem3.setSelected(true);
        jRadioButtonMenuItem3.setText(bundle.getString("ChartJDialog_Light")); // NOI18N
        jRadioButtonMenuItem3.addActionListener(new java.awt.event.ActionListener() {
            public void actionPerformed(java.awt.event.ActionEvent evt) {
                jRadioButtonMenuItem3ActionPerformed(evt);
            }
        });
        jMenu4.add(jRadioButtonMenuItem3);

        buttonGroup2.add(jRadioButtonMenuItem4);
        jRadioButtonMenuItem4.setText(bundle.getString("ChartJDialog_Dark")); // NOI18N
        jRadioButtonMenuItem4.addActionListener(new java.awt.event.ActionListener() {
            public void actionPerformed(java.awt.event.ActionEvent evt) {
                jRadioButtonMenuItem4ActionPerformed(evt);
            }
        });
        jMenu4.add(jRadioButtonMenuItem4);

        jMenuBar1.add(jMenu4);

        jMenu8.setText(bundle.getString("ChartJDialog_Help")); // NOI18N

        jMenuItem7.setText(bundle.getString("ChartJDialog_TechnicalAnalysisTutorial")); // NOI18N
        jMenuItem7.addActionListener(new java.awt.event.ActionListener() {
            public void actionPerformed(java.awt.event.ActionEvent evt) {
                jMenuItem7ActionPerformed(evt);
            }
        });
        jMenu8.add(jMenuItem7);

        jMenuBar1.add(jMenu8);

        setJMenuBar(jMenuBar1);

        setSize(new java.awt.Dimension(750, 600));
        setLocationRelativeTo(null);
    }// </editor-fold>//GEN-END:initComponents

    private void initKeyBindings() {
        KeyStroke escapeKeyStroke = KeyStroke.getKeyStroke(java.awt.event.KeyEvent.VK_ESCAPE, 0, false);
        Action escapeAction = new AbstractAction() {
            // close the frame when the user presses escape
            public void actionPerformed(ActionEvent e) {
                dispose();
            }
        };

        getRootPane().getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(escapeKeyStroke, "ESCAPE");
        getRootPane().getActionMap().put("ESCAPE", escapeAction);
    }

    private ComboBoxModel getComboBoxModel() {
        return new javax.swing.DefaultComboBoxModel(new String[] { GUIBundle.getString("ChartJDialog_Daily"),
                GUIBundle.getString("ChartJDialog_Weekly"), GUIBundle.getString("ChartJDialog_Monthly") });
    }

    private void changeInterval(Interval interval) {
        if (interval == getCurrentInterval()) {
            // Same as current interval. Nothing to do. Returns early.
            return;
        }

        if (interval == Interval.Weekly) {
            this.chartDatas = org.yccheok.jstock.charting.Utils.getWeeklyChartData(this.stockHistoryServer);
        } else if (interval == Interval.Monthly) {
            this.chartDatas = org.yccheok.jstock.charting.Utils.getMonthlyChartData(this.stockHistoryServer);
        } else {
            assert (interval == Interval.Daily);
            this.chartDatas = org.yccheok.jstock.charting.Utils.getDailyChartData(this.stockHistoryServer);
        }

        this.priceTimeSeries = this.getPriceTimeSeries(this.chartDatas);
        this.priceDataset = new TimeSeriesCollection(this.priceTimeSeries);
        this.priceOHLCDataset = this.getOHLCDataset(this.chartDatas);
        this.volumeDataset = this.getVolumeDataset(this.chartDatas);
        // Throw away all the old data.
        this.priceVolumeChart = null;
        this.candlestickChart = null;
        changeType(JStock.instance().getChartJDialogOptions().getType());
        // and throw away all the old TAs.
        time_series_moving_average_map.clear();
        time_series_exponential_moving_average_map.clear();
        xydata_index_map.clear();
        activeTAExs.clear();
        price_volume_ta_map.clear();
        candlestick_ta_map.clear();
        // We need to rebuild TA menus.
        this.jMenu2.removeAll();

        /* Remember the setting. */
        JStock.instance().getChartJDialogOptions().setInterval(interval);
        /* Remember current interval. */
        this.currentInverval = interval;

        // Update the GUI.
        // Only done this after "Remember current interval".
        // As this will trigger actionPerformed event. We will use currentInverval
        // to prevent it from re-entering this method.
        if (interval == Interval.Daily) {
            this.jComboBox1.setSelectedItem(GUIBundle.getString("ChartJDialog_Daily"));
        } else if (interval == Interval.Weekly) {
            this.jComboBox1.setSelectedItem(GUIBundle.getString("ChartJDialog_Weekly"));
        } else if (interval == Interval.Monthly) {
            this.jComboBox1.setSelectedItem(GUIBundle.getString("ChartJDialog_Monthly"));
        }

        new Thread(new Runnable() {
            @Override
            public void run() {
                SwingUtilities.invokeLater(new Runnable() {
                    @Override
                    public void run() {
                        loadChartJDialogOptions();
                    }
                });
            }
        }).start();
    }

    /**
     * Changes the type (price volume or candlestick chart) of this chart dialog.
     * @param the type (price volume or candlestick chart) of this chart dialog to be changed.
     */
    private void changeType(Type type) {
        if (type == this.getCurrentType()) {
            if (this.priceVolumeChart == null && type == Type.PriceVolume) {
                // Do not returns early! Chart creation is needed.
            } else if (this.candlestickChart == null && type == Type.Candlestick) {
                // Do not returns early! Chart creation is needed.
            } else {
                // Same as current type. Nothing to do. Returns early.
                return;
            }
        }

        if (type == Type.PriceVolume) {
            if (this.priceVolumeChart == null) {
                this.priceVolumeChart = this.createPriceVolumeChart(this.priceDataset, this.volumeDataset);
            }
            chartPanel.setChart(this.priceVolumeChart);
            // Make chartPanel able to receive key event.
            // So that we may use arrow key to move around yellow information box.
            chartPanel.requestFocus();
            if (this.chartLayerUI != null) {
                this.chartLayerUI.updateTraceInfos();
            }
        } else if (type == Type.Candlestick) {
            if (this.candlestickChart == null) {
                this.candlestickChart = this.createCandlestickChart(this.priceOHLCDataset);
            }
            chartPanel.setChart(this.candlestickChart);
            // Make chartPanel able to receive key event.
            // So that we may use arrow key to move around yellow information box.
            chartPanel.requestFocus();
            if (this.chartLayerUI != null) {
                this.chartLayerUI.updateTraceInfos();
            }
        }

        /* Remember the setting. */
        JStock.instance().getChartJDialogOptions().setType(type);
        /* Remember current interval. */
        this.currentType = type;

        /* Update the GUI. */
        if (type == Type.PriceVolume) {
            this.jRadioButtonMenuItem1.setSelected(true);
        } else if (type == Type.Candlestick) {
            this.jRadioButtonMenuItem2.setSelected(true);
        }
    }

    private void jComboBox1ActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_jComboBox1ActionPerformed
        final String selected = this.jComboBox1.getSelectedItem().toString();
        Interval interval = Interval.Daily;
        if (selected.equals(GUIBundle.getString("ChartJDialog_Daily"))) {
            interval = Interval.Daily;
        } else if (selected.equals(GUIBundle.getString("ChartJDialog_Weekly"))) {
            interval = Interval.Weekly;
        } else if (selected.equals(GUIBundle.getString("ChartJDialog_Monthly"))) {
            interval = Interval.Monthly;
        }
        this.changeInterval(interval);
    }//GEN-LAST:event_jComboBox1ActionPerformed

    private void jMenuItem1ActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_jMenuItem1ActionPerformed
        assert (this.stockHistoryServer.size() > 0);
        final Stock stock = this.stockHistoryServer.getStock(this.stockHistoryServer.getTimestamp(0));
        final File file = org.yccheok.jstock.gui.Utils.promptSaveCSVAndExcelJFileChooser(stock.code.toString());

        if (file != null) {
            if (org.yccheok.jstock.gui.Utils.getFileExtension(file).equals("csv")) {
                final Statements statements = Statements.newInstanceFromStockHistoryServer(stockHistoryServer,
                        false);
                statements.saveAsCSVFile(file);
            } else if (org.yccheok.jstock.gui.Utils.getFileExtension(file).equals("xls")) {
                final Statements statements = Statements.newInstanceFromStockHistoryServer(stockHistoryServer,
                        false);
                statements.saveAsExcelFile(file, stock.code.toString());
            } else {
                assert (false);
            }
        }
    }//GEN-LAST:event_jMenuItem1ActionPerformed

    /**
     * Clear all technical analysis information.
     */
    private void clearAll() {
        for (int i = 0; i < this.activeTAExs.size(); i++) {
            final TAEx taEx = this.activeTAExs.get(i);
            if (taEx.ta == TA.SMA) {
                this.updateSMA((Integer) taEx.parameter, false);
                i--;
            } else if (taEx.ta == TA.RSI) {
                this.updateRSI((Integer) taEx.parameter, false);
                i--;
            } else if (taEx.ta == TA.CCI) {
                this.updateCCI((Integer) taEx.parameter, false);
                i--;
            } else if (taEx.ta == TA.EMA) {
                this.updateEMA((Integer) taEx.parameter, false);
                i--;
            } else if (taEx.ta == TA.MFI) {
                this.updateMFI((Integer) taEx.parameter, false);
                i--;
            } else if (taEx.ta == TA.MACD) {
                this.updateMACD((MACD.Period) taEx.parameter, false);
                i--;
            } else {
                assert (false);
            }
        }
        // Update menus.
        for (int i = 0, count = jMenu2.getItemCount(); i < count; i++) {
            final JMenuItem item = jMenu2.getItem(i);
            if (item instanceof JMenu) {
                final JMenu m = (JMenu) item;
                for (int j = 0, count2 = m.getItemCount(); j < count2; j++) {
                    JMenuItem item2 = m.getItem(j);
                    if (item2 instanceof javax.swing.JCheckBoxMenuItem) {
                        m.getItem(j).setSelected(false);
                    }
                }
            }

        }
    }

    private void jMenuItem7ActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_jMenuItem7ActionPerformed
        org.yccheok.jstock.gui.Utils
                .launchWebBrowser(Utils.getURL(org.yccheok.jstock.network.Utils.Type.MA_INDICATOR_HTML));
    }//GEN-LAST:event_jMenuItem7ActionPerformed

    private void jLabel9MouseClicked(java.awt.event.MouseEvent evt) {//GEN-FIRST:event_jLabel9MouseClicked
        this.zoom(Zoom.Days7);
    }//GEN-LAST:event_jLabel9MouseClicked

    /**
     * Calculate and update high low value labels, according to current displayed
     * time range. This is a time consuming method, and shall be called by
     * user thread.
     */
    private void _updateHighLowJLabels() {
        SwingUtilities.invokeLater(new Runnable() {
            @Override
            public void run() {
                ChartJDialog.this.jLabel2.setText("");
                ChartJDialog.this.jLabel4.setText("");
            }
        });

        final ValueAxis valueAxis = this.getPlot().getDomainAxis();
        final Range range = valueAxis.getRange();
        final long lowerBound = (long) range.getLowerBound();
        final long upperBound = (long) range.getUpperBound();

        // Perform binary search, to located day in price time series, which
        // is equal or lesser than upperBound.
        int low = 0;
        int high = this.priceTimeSeries.getItemCount() - 1;
        long best_dist = Long.MAX_VALUE;
        int best_mid = -1;
        while (low <= high) {
            int mid = (low + high) >>> 1;
            final Day day = (Day) this.priceTimeSeries.getDataItem(mid).getPeriod();
            long v = day.getFirstMillisecond();

            if (v > upperBound) {
                high = mid - 1;
            } else if (v < upperBound) {
                low = mid + 1;
                long dist = upperBound - v;
                if (dist < best_dist) {
                    best_dist = dist;
                    best_mid = mid;
                }
            } else {
                best_dist = 0;
                best_mid = mid;
                break;
            }
        }

        if (best_mid < 0) {
            return;
        }

        double high_price = -Double.MAX_VALUE;
        double low_price = Double.MAX_VALUE;
        final DefaultHighLowDataset defaultHighLowDataset = (DefaultHighLowDataset) this.priceOHLCDataset;
        for (int i = best_mid; i >= 0; i--) {
            final TimeSeriesDataItem item = this.priceTimeSeries.getDataItem(i);
            final long time = ((Day) item.getPeriod()).getFirstMillisecond();
            if (time < lowerBound) {
                break;
            }

            final double _high_price = defaultHighLowDataset.getHighValue(0, i);
            final double _low_price = defaultHighLowDataset.getLowValue(0, i);
            final double _last_price = defaultHighLowDataset.getCloseValue(0, i);

            high_price = Math.max(high_price, _high_price);

            // Prevent bad data.
            if (_low_price > 0) {
                low_price = Math.min(low_price, _low_price);
            } else {
                if (_high_price > 0) {
                    low_price = Math.min(low_price, _high_price);
                }
                if (_last_price > 0) {
                    low_price = Math.min(low_price, _last_price);
                }
            }
        }

        final double h = high_price;
        final double l = low_price;
        if (high_price >= low_price) {
            SwingUtilities.invokeLater(new Runnable() {
                @Override
                public void run() {
                    ChartJDialog.this.jLabel2.setText(org.yccheok.jstock.gui.Utils.stockPriceDecimalFormat(h));
                    ChartJDialog.this.jLabel4.setText(org.yccheok.jstock.gui.Utils.stockPriceDecimalFormat(l));
                }
            });
        }
    }

    /**
     * Reset all day labels back to plain font.
     */
    private void resetAllDayLabels() {
        JLabel[] labels = { jLabel9, jLabel10, jLabel11, jLabel12, jLabel13, jLabel14, jLabel5, jLabel6 };
        for (JLabel label : labels) {
            final Font oldFont = label.getFont();
            // Reset BOLD attribute.
            final Font font = oldFont.deriveFont(oldFont.getStyle() & ~Font.BOLD);
            label.setFont(font);
        }
    }

    /**
     * Calculate and update high low value labels, according to current displayed
     * time range. This method will return immediately, as the calculating and
     * updating task by performed by user thread.
     */
    private void updateHighLowJLabels() {
        updateHighLowLabelsPool.execute(new Runnable() {
            @Override
            public void run() {
                ChartJDialog.this._updateHighLowJLabels();
            }
        });
    }

    /**
     * Zoom in to this chart according to zoom information, and update day labels
     * as well.
     * @param zoom zoom information
     */
    private void zoom(Zoom z) {
        switch (z) {
        case Days7:
            this._zoom(Calendar.DATE, -7);
            /* Reset first. */
            this.resetAllDayLabels();
            this.jLabel9.setFont(org.yccheok.jstock.gui.Utils.getBoldFont(this.jLabel9.getFont()));
            break;

        case Month1:
            this._zoom(Calendar.MONTH, -1);
            /* Reset first. */
            this.resetAllDayLabels();
            this.jLabel10.setFont(org.yccheok.jstock.gui.Utils.getBoldFont(this.jLabel10.getFont()));
            break;

        case Months3:
            this._zoom(Calendar.MONTH, -3);
            /* Reset first. */
            this.resetAllDayLabels();
            this.jLabel11.setFont(org.yccheok.jstock.gui.Utils.getBoldFont(this.jLabel11.getFont()));
            break;

        case Months6:
            this._zoom(Calendar.MONTH, -6);
            /* Reset first. */
            this.resetAllDayLabels();
            this.jLabel12.setFont(org.yccheok.jstock.gui.Utils.getBoldFont(this.jLabel12.getFont()));
            break;

        case Year1:
            this._zoom(Calendar.YEAR, -1);
            /* Reset first. */
            this.resetAllDayLabels();
            this.jLabel13.setFont(org.yccheok.jstock.gui.Utils.getBoldFont(this.jLabel13.getFont()));
            break;

        case Year5:
            this._zoom(Calendar.YEAR, -5);
            /* Reset first. */
            this.resetAllDayLabels();
            this.jLabel5.setFont(org.yccheok.jstock.gui.Utils.getBoldFont(this.jLabel5.getFont()));
            break;

        case Year10:
            this._zoom(Calendar.YEAR, -10);
            /* Reset first. */
            this.resetAllDayLabels();
            this.jLabel6.setFont(org.yccheok.jstock.gui.Utils.getBoldFont(this.jLabel6.getFont()));
            break;

        case All:
            this.chartPanel.restoreAutoBounds();
            /* Reset first. */
            this.resetAllDayLabels();
            /* Bold the target. */
            this.jLabel14.setFont(org.yccheok.jstock.gui.Utils.getBoldFont(this.jLabel14.getFont()));
            break;
        }
        JStock.instance().getChartJDialogOptions().setZoom(z);
    }

    /**
     * Zoom in to this chart with specific amount of time.
     * @param field the calendar field.
     * @param amount the amount of date or time to be added to the field.
     */
    private void _zoom(int field, int amount) {
        this.chartPanel.restoreAutoBounds();

        final int itemCount = this.priceTimeSeries.getItemCount();
        final Day day = (Day) this.priceTimeSeries.getDataItem(itemCount - 1).getPeriod();
        // Candle stick takes up half day space.
        // Volume price chart's volume information takes up whole day space.
        final long end = day.getFirstMillisecond()
                + (this.getCurrentType() == Type.Candlestick ? (1000 * 60 * 60 * 12) : (1000 * 60 * 60 * 24 - 1));
        final Calendar calendar = Calendar.getInstance();
        // -1. Calendar's month is 0 based but JFreeChart's month is 1 based.
        calendar.set(day.getYear(), day.getMonth() - 1, day.getDayOfMonth(), 0, 0, 0);
        calendar.set(Calendar.MILLISECOND, 0);

        calendar.add(field, amount);
        // Candle stick takes up half day space.
        // Volume price chart's volume information does not take up any space.
        final long start = Math.max(0, calendar.getTimeInMillis()
                - (this.getCurrentType() == Type.Candlestick ? (1000 * 60 * 60 * 12) : 0));
        final ValueAxis valueAxis = this.getPlot().getDomainAxis();

        if (priceTimeSeries.getItemCount() > 0) {
            if (start < priceTimeSeries.getTimePeriod(0).getFirstMillisecond()) {
                // To prevent zoom-out too much.
                // This happens when user demands for 10 years zoom, where we
                // are only having 5 years data.
                return;
            }
        }

        valueAxis.setRange(start, end);

        double min = Double.MAX_VALUE;
        double max = Double.MIN_VALUE;
        double max_volume = Double.MIN_VALUE;
        final DefaultHighLowDataset defaultHighLowDataset = (DefaultHighLowDataset) this.priceOHLCDataset;

        for (int i = itemCount - 1; i >= 0; i--) {
            final TimeSeriesDataItem item = this.priceTimeSeries.getDataItem(i);
            final Day d = (Day) item.getPeriod();
            if (d.getFirstMillisecond() < start) {
                break;
            }

            final double high = defaultHighLowDataset.getHighValue(0, i);
            final double low = defaultHighLowDataset.getLowValue(0, i);
            final double volume = defaultHighLowDataset.getVolumeValue(0, i);

            if (max < high) {
                max = high;
            }
            if (min > low) {
                min = low;
            }
            if (max_volume < volume) {
                max_volume = volume;
            }
        }

        if (min > max) {
            return;
        }

        final ValueAxis rangeAxis = this.getPlot().getRangeAxis();
        final Range rangeAxisRange = rangeAxis.getRange();
        // Increase each side by 1%
        double tolerance = 0.01 * (max - min);
        // The tolerance must within range [0.01, 1.0]
        tolerance = Math.min(Math.max(0.01, tolerance), 1.0);
        // The range must within the original chart range.
        min = Math.max(rangeAxisRange.getLowerBound(), min - tolerance);
        max = Math.min(rangeAxisRange.getUpperBound(), max + tolerance);

        this.getPlot().getRangeAxis().setRange(min, max);

        if (this.getPlot().getRangeAxisCount() > 1) {
            final double volumeUpperBound = this.getPlot().getRangeAxis(1).getRange().getUpperBound();
            final double suggestedVolumneUpperBound = max_volume * 4;
            // To prevent over zoom-in.
            if (suggestedVolumneUpperBound < volumeUpperBound) {
                this.getPlot().getRangeAxis(1).setRange(0, suggestedVolumneUpperBound);
            }
        }
    }

    private void jLabel10MouseClicked(java.awt.event.MouseEvent evt) {//GEN-FIRST:event_jLabel10MouseClicked
        this.zoom(Zoom.Month1);
    }//GEN-LAST:event_jLabel10MouseClicked

    private void jLabel11MouseClicked(java.awt.event.MouseEvent evt) {//GEN-FIRST:event_jLabel11MouseClicked
        this.zoom(Zoom.Months3);
    }//GEN-LAST:event_jLabel11MouseClicked

    private void jLabel12MouseClicked(java.awt.event.MouseEvent evt) {//GEN-FIRST:event_jLabel12MouseClicked
        this.zoom(Zoom.Months6);
    }//GEN-LAST:event_jLabel12MouseClicked

    private void jLabel13MouseClicked(java.awt.event.MouseEvent evt) {//GEN-FIRST:event_jLabel13MouseClicked
        this.zoom(Zoom.Year1);
    }//GEN-LAST:event_jLabel13MouseClicked

    private void jLabel14MouseClicked(java.awt.event.MouseEvent evt) {//GEN-FIRST:event_jLabel14MouseClicked
        this.zoom(Zoom.All);
    }//GEN-LAST:event_jLabel14MouseClicked

    private void jMenu2MenuSelected(javax.swing.event.MenuEvent evt) {//GEN-FIRST:event_jMenu2MenuSelected
        if (this.jMenu2.getItemCount() <= 0) {
            this.buildTAMenuItems();
        }
    }//GEN-LAST:event_jMenu2MenuSelected

    private void jRadioButtonMenuItem1ActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_jRadioButtonMenuItem1ActionPerformed
        this.changeType(Type.PriceVolume);
        this.zoom(JStock.instance().getChartJDialogOptions().getZoom());
    }//GEN-LAST:event_jRadioButtonMenuItem1ActionPerformed

    private void jRadioButtonMenuItem2ActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_jRadioButtonMenuItem2ActionPerformed
        this.changeType(Type.Candlestick);
        this.zoom(JStock.instance().getChartJDialogOptions().getZoom());
    }//GEN-LAST:event_jRadioButtonMenuItem2ActionPerformed

    private void jLabel5MouseClicked(java.awt.event.MouseEvent evt) {//GEN-FIRST:event_jLabel5MouseClicked
        this.zoom(Zoom.Year5);
    }//GEN-LAST:event_jLabel5MouseClicked

    private void jLabel6MouseClicked(java.awt.event.MouseEvent evt) {//GEN-FIRST:event_jLabel6MouseClicked
        this.zoom(Zoom.Year10);
    }//GEN-LAST:event_jLabel6MouseClicked

    private void jRadioButtonMenuItem3ActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_jRadioButtonMenuItem3ActionPerformed
        JStock.instance().getJStockOptions().setChartTheme(JStockOptions.ChartTheme.Light);
        if (priceVolumeChart != null) {
            org.yccheok.jstock.charting.Utils.applyChartThemeEx(priceVolumeChart);
        }
        if (candlestickChart != null) {
            org.yccheok.jstock.charting.Utils.applyChartThemeEx(candlestickChart);
        }
    }//GEN-LAST:event_jRadioButtonMenuItem3ActionPerformed

    private void jRadioButtonMenuItem4ActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_jRadioButtonMenuItem4ActionPerformed
        JStock.instance().getJStockOptions().setChartTheme(JStockOptions.ChartTheme.Dark);
        if (priceVolumeChart != null) {
            org.yccheok.jstock.charting.Utils.applyChartThemeEx(priceVolumeChart);
        }
        if (candlestickChart != null) {
            org.yccheok.jstock.charting.Utils.applyChartThemeEx(candlestickChart);
        }
    }//GEN-LAST:event_jRadioButtonMenuItem4ActionPerformed

    private void formWindowClosing(java.awt.event.WindowEvent evt) {//GEN-FIRST:event_formWindowClosing
        // To avoid memory leak.
        parent.removeWindowListener(this);
        this.dispose();
    }//GEN-LAST:event_formWindowClosing

    /**
     * Creates a chart.
     *
     * @return a chart.
     */
    private JFreeChart createPriceVolumeChart(XYDataset priceDataset, XYDataset volumeDataset) {
        final String title = getBestStockName();

        final ValueAxis timeAxis = new DateAxis(GUIBundle.getString("ChartJDialog_Date"));
        timeAxis.setLowerMargin(0.02); // reduce the default margins
        timeAxis.setUpperMargin(0.02);

        final NumberAxis rangeAxis1 = new NumberAxis(GUIBundle.getString("ChartJDialog_Price"));
        rangeAxis1.setAutoRangeIncludesZero(false); // override default
        rangeAxis1.setLowerMargin(0.40); // to leave room for volume bars
        DecimalFormat format = new DecimalFormat("0.00#");
        rangeAxis1.setNumberFormatOverride(format);

        XYPlot plot = new XYPlot(priceDataset, timeAxis, rangeAxis1, null);

        XYItemRenderer renderer1 = new XYLineAndShapeRenderer(true, false);
        renderer1.setBaseToolTipGenerator(
                new StandardXYToolTipGenerator(StandardXYToolTipGenerator.DEFAULT_TOOL_TIP_FORMAT,
                        new SimpleDateFormat("d-MMM-yyyy"), new DecimalFormat("0.00#")));
        plot.setRenderer(0, renderer1);

        final NumberAxis rangeAxis2 = new NumberAxis("Volume");
        rangeAxis2.setUpperMargin(1.00); // to leave room for price line
        plot.setRangeAxis(1, rangeAxis2);
        plot.setDataset(1, volumeDataset);
        plot.mapDatasetToRangeAxis(1, 1);

        XYBarRenderer renderer2 = new XYBarRenderer(0.20);
        renderer2.setBaseToolTipGenerator(
                new StandardXYToolTipGenerator(StandardXYToolTipGenerator.DEFAULT_TOOL_TIP_FORMAT,
                        new SimpleDateFormat("d-MMM-yyyy"), new DecimalFormat("0,000.00")));
        plot.setRenderer(1, renderer2);

        CombinedDomainXYPlot cplot = new CombinedDomainXYPlot(timeAxis);
        cplot.add(plot, 1);
        cplot.setGap(8.0);

        JFreeChart chart = new JFreeChart(title, JFreeChart.DEFAULT_TITLE_FONT, cplot, true);
        org.yccheok.jstock.charting.Utils.applyChartThemeEx(chart);

        // Only do it after applying chart theme.
        org.yccheok.jstock.charting.Utils.setPriceSeriesPaint(renderer1);
        org.yccheok.jstock.charting.Utils.setVolumeSeriesPaint(renderer2);

        // Handle zooming event.
        chart.addChangeListener(this.getChartChangeListner());

        return chart;
    }

    private TimeSeries getPriceTimeSeries(List<ChartData> chartDatas) {
        // create dataset 1...
        TimeSeries series1 = new TimeSeries(GUIBundle.getString("ChartJDialog_Price"));

        for (ChartData chartData : chartDatas) {
            series1.add(new Day(new Date(chartData.timestamp)), chartData.lastPrice);
        }
        return series1;
    }

    /**
     * Creates a sample dataset.
     *
     * @return A sample dataset.
     */
    private XYDataset getVolumeDataset(List<ChartData> chartDatas) {

        // create dataset 2...
        TimeSeries series1 = new TimeSeries(GUIBundle.getString("ChartJDialog_Volume"));

        for (ChartData chartData : chartDatas) {
            series1.add(new Day(new Date(chartData.timestamp)), chartData.volume);
        }

        return new TimeSeriesCollection(series1);
    }

    private String getBestStockName() {
        final int num = stockHistoryServer.size();
        final Stock stock = stockHistoryServer.getStock(stockHistoryServer.getTimestamp(num - 1));

        if (org.yccheok.jstock.engine.Utils.isNameImmutable()) {
            final StockNameDatabase stockNameDatabase = JStock.instance().getStockNameDatabase();
            if (stockNameDatabase != null) {
                final String name = stockNameDatabase.codeToName(stock.code);
                if (name != null) {
                    return name;
                }
            }
        }

        final String name = stock.getName();
        // For unknown reason, server may return us empty stock name.
        if (name.isEmpty()) {
            final Symbol symbol = stock.symbol;
            if (false == symbol.toString().isEmpty()) {
                // Luckly. The symbol is not empty. Use it as replacement to
                // name.
                return symbol.toString();
            }
            // Symbol from server is empty. We need to ask help from offline
            // database.
            final StockInfoDatabase stockInfoDatabase = JStock.instance().getStockInfoDatabase();
            if (stockInfoDatabase != null) {
                final Symbol s = stockInfoDatabase.codeToSymbol(stock.code);
                if (s != null) {
                    // Use symbol as replacement if possible.
                    return s.toString();
                }
            }
            // If not, we will just apply code as replacement.
            return stock.code.toString();
        }

        return name;
    }

    /**
     * Creates a chart.
     * 
     * @param dataset  the dataset.
     * 
     * @return The dataset.
     */
    private JFreeChart createCandlestickChart(OHLCDataset priceOHLCDataset) {
        final String title = getBestStockName();

        final ValueAxis timeAxis = new DateAxis(GUIBundle.getString("ChartJDialog_Date"));
        final NumberAxis valueAxis = new NumberAxis(GUIBundle.getString("ChartJDialog_Price"));
        valueAxis.setAutoRangeIncludesZero(false);
        valueAxis.setUpperMargin(0.0);
        valueAxis.setLowerMargin(0.0);
        XYPlot plot = new XYPlot(priceOHLCDataset, timeAxis, valueAxis, null);

        final CandlestickRenderer candlestickRenderer = new CandlestickRenderer();
        plot.setRenderer(candlestickRenderer);

        // Give good width when zoom in, but too slow in calculation.
        ((CandlestickRenderer) plot.getRenderer()).setAutoWidthMethod(CandlestickRenderer.WIDTHMETHOD_SMALLEST);

        CombinedDomainXYPlot cplot = new CombinedDomainXYPlot(timeAxis);
        cplot.add(plot, 3);
        cplot.setGap(8.0);

        JFreeChart chart = new JFreeChart(title, JFreeChart.DEFAULT_TITLE_FONT, cplot, true);

        org.yccheok.jstock.charting.Utils.applyChartThemeEx(chart);

        // Handle zooming event.
        chart.addChangeListener(this.getChartChangeListner());

        return chart;
    }

    /**
     * Creates a sample high low dataset.
     *
     * @return a sample high low dataset.
     */
    private OHLCDataset getOHLCDataset(List<ChartData> chartDatas) {

        final int size = chartDatas.size();

        Date[] date = new Date[size];
        double[] high = new double[size];
        double[] low = new double[size];
        double[] open = new double[size];
        double[] close = new double[size];
        double[] volume = new double[size];

        int i = 0;
        for (ChartData chartData : chartDatas) {
            date[i] = new Date(chartData.timestamp);
            high[i] = chartData.highPrice;
            low[i] = chartData.lowPrice;
            open[i] = chartData.openPrice;
            close[i] = chartData.lastPrice;
            volume[i] = chartData.volume;
            i++;
        }

        return new DefaultHighLowDataset(GUIBundle.getString("ChartJDialog_Price"), date, high, low, open, close,
                volume);
    }

    // For Candlestick chart usage.
    private int getIndex(TAEx taEx, Map<TAEx, Integer> map) {
        if (map.containsKey(taEx)) {
            return map.get(taEx);
        }
        int max = 0; // 0, reserve for price and volume.
                     // Price and volume are using same index for Candlestick.
        for (Integer index : map.values()) {
            if (index > max) {
                max = index;
            }
        }
        return max + 1;
    }

    private String getEMAKey(int days) {
        Interval interval = this.getCurrentInterval();
        String c = "d";
        if (interval == Interval.Weekly) {
            c = "w";
        } else if (interval == Interval.Monthly) {
            c = "m";
        }
        return days + c + " EMA";
    }

    private String getSMAKey(int days) {
        Interval interval = this.getCurrentInterval();
        String c = "d";
        if (interval == Interval.Weekly) {
            c = "w";
        } else if (interval == Interval.Monthly) {
            c = "m";
        }
        return days + c + " SMA";
    }

    private String getRSIKey(int days) {
        Interval interval = this.getCurrentInterval();
        String c = "d";
        if (interval == Interval.Weekly) {
            c = "w";
        } else if (interval == Interval.Monthly) {
            c = "m";
        }
        return days + c + " RSI";
    }

    private String getMACDKey(MACD.Period period) {
        Interval interval = this.getCurrentInterval();
        String c = "d";
        if (interval == Interval.Weekly) {
            c = "w";
        } else if (interval == Interval.Monthly) {
            c = "m";
        }
        return "(" + period.fastPeriod + "," + period.slowPeriod + "," + period.period + ")";
    }

    private String getCCIKey(int days) {
        Interval interval = this.getCurrentInterval();
        String c = "d";
        if (interval == Interval.Weekly) {
            c = "w";
        } else if (interval == Interval.Monthly) {
            c = "m";
        }
        return days + c + " CCI";
    }

    private String getMFIKey(int days) {
        Interval interval = this.getCurrentInterval();
        String c = "d";
        if (interval == Interval.Weekly) {
            c = "w";
        } else if (interval == Interval.Monthly) {
            c = "m";
        }
        return days + c + " MFI";
    }

    private void updateCCI(int days, boolean show) {
        if (this.priceVolumeChart == null) {
            this.priceVolumeChart = this.createPriceVolumeChart(this.priceDataset, this.volumeDataset);
        }
        if (this.candlestickChart == null) {
            this.candlestickChart = this.createCandlestickChart(this.priceOHLCDataset);
        }

        final TAEx taEx = TAEx.newInstance(TA.CCI, new Integer(days));

        if (show) {
            if (price_volume_ta_map.containsKey(taEx) == false) {
                final XYDataset dataset = org.yccheok.jstock.charting.TechnicalAnalysis.createCCI(this.chartDatas,
                        getCCIKey(days), days);
                NumberAxis rangeAxis1 = new NumberAxis(GUIBundle.getString("ChartJDialog_CCI"));
                rangeAxis1.setAutoRangeIncludesZero(false); // override default
                rangeAxis1.setLowerMargin(0.40); // to leave room for volume bars
                DecimalFormat format = new DecimalFormat("0");
                rangeAxis1.setNumberFormatOverride(format);

                final ValueAxis timeAxis = new DateAxis(GUIBundle.getString("ChartJDialog_Date"));
                timeAxis.setLowerMargin(0.02); // reduce the default margins
                timeAxis.setUpperMargin(0.02);

                XYPlot plot = new XYPlot(dataset, timeAxis, rangeAxis1, null);

                XYItemRenderer renderer1 = new XYLineAndShapeRenderer(true, false);
                renderer1.setBaseToolTipGenerator(
                        new StandardXYToolTipGenerator(StandardXYToolTipGenerator.DEFAULT_TOOL_TIP_FORMAT,
                                new SimpleDateFormat("d-MMM-yyyy"), new DecimalFormat("0.00#")));
                plot.setRenderer(0, renderer1);
                org.yccheok.jstock.charting.Utils.setPriceSeriesPaint(renderer1);
                price_volume_ta_map.put(taEx, plot);
            }

            if (candlestick_ta_map.containsKey(taEx) == false) {
                try {
                    /* Not sure why. I cannot make priceVolumeChart and candlestickChart sharing the same
                     * plot. If not, this will inhibit incorrect zooming behavior.
                     */
                    candlestick_ta_map.put(taEx, (XYPlot) price_volume_ta_map.get(taEx).clone());
                } catch (CloneNotSupportedException ex) {
                    log.error(null, ex);
                }
            }

            if (this.activeTAExs.contains(taEx) == false) {
                // Avoid duplication.
                final XYPlot price_volume_ta = price_volume_ta_map.get(taEx);
                final XYPlot candlestick_ta = candlestick_ta_map.get(taEx);

                final CombinedDomainXYPlot cplot0 = (CombinedDomainXYPlot) this.priceVolumeChart.getPlot();
                final CombinedDomainXYPlot cplot1 = (CombinedDomainXYPlot) this.candlestickChart.getPlot();
                if (price_volume_ta != null)
                    cplot0.add(price_volume_ta, 1); // weight is 1.
                if (candlestick_ta != null)
                    cplot1.add(candlestick_ta, 1); // weight is 1.
                org.yccheok.jstock.charting.Utils.applyChartThemeEx(this.priceVolumeChart);
                org.yccheok.jstock.charting.Utils.applyChartThemeEx(this.candlestickChart);
            }
        } else {
            final CombinedDomainXYPlot cplot0 = (CombinedDomainXYPlot) this.priceVolumeChart.getPlot();
            final CombinedDomainXYPlot cplot1 = (CombinedDomainXYPlot) this.candlestickChart.getPlot();
            final XYPlot price_volume_ta = price_volume_ta_map.get(taEx);
            final XYPlot candlestick_ta = candlestick_ta_map.get(taEx);

            if (price_volume_ta != null)
                cplot0.remove(price_volume_ta);
            if (candlestick_ta != null)
                cplot1.remove(candlestick_ta);
        }

        if (show && this.activeTAExs.contains(taEx) == false) {
            this.activeTAExs.add(taEx);
            JStock.instance().getChartJDialogOptions().add(taEx);
        } else if (!show) {
            this.activeTAExs.remove(taEx);
            JStock.instance().getChartJDialogOptions().remove(taEx);
        }
    }

    private void updateMFI(int days, boolean show) {
        if (this.priceVolumeChart == null) {
            this.priceVolumeChart = this.createPriceVolumeChart(this.priceDataset, this.volumeDataset);
        }
        if (this.candlestickChart == null) {
            this.candlestickChart = this.createCandlestickChart(this.priceOHLCDataset);
        }

        final TAEx taEx = TAEx.newInstance(TA.MFI, new Integer(days));

        if (show) {
            if (price_volume_ta_map.containsKey(taEx) == false) {
                final XYDataset dataset = org.yccheok.jstock.charting.TechnicalAnalysis.createMFI(this.chartDatas,
                        getMFIKey(days), days);
                NumberAxis rangeAxis1 = new NumberAxis(GUIBundle.getString("ChartJDialog_MFI"));
                rangeAxis1.setAutoRangeIncludesZero(false); // override default
                rangeAxis1.setLowerMargin(0.40); // to leave room for volume bars
                DecimalFormat format = new DecimalFormat("0");
                rangeAxis1.setNumberFormatOverride(format);

                final ValueAxis timeAxis = new DateAxis(GUIBundle.getString("ChartJDialog_Date"));
                timeAxis.setLowerMargin(0.02); // reduce the default margins
                timeAxis.setUpperMargin(0.02);

                XYPlot plot = new XYPlot(dataset, timeAxis, rangeAxis1, null);

                XYItemRenderer renderer1 = new XYLineAndShapeRenderer(true, false);
                renderer1.setBaseToolTipGenerator(
                        new StandardXYToolTipGenerator(StandardXYToolTipGenerator.DEFAULT_TOOL_TIP_FORMAT,
                                new SimpleDateFormat("d-MMM-yyyy"), new DecimalFormat("0.00#")));
                plot.setRenderer(0, renderer1);
                price_volume_ta_map.put(taEx, plot);
            }

            if (candlestick_ta_map.containsKey(taEx) == false) {
                try {
                    /* Not sure why. I cannot make priceVolumeChart and candlestickChart sharing the same
                     * plot. If not, this will inhibit incorrect zooming behavior.
                     */
                    candlestick_ta_map.put(taEx, (XYPlot) price_volume_ta_map.get(taEx).clone());
                } catch (CloneNotSupportedException ex) {
                    log.error(null, ex);
                }
            }

            if (this.activeTAExs.contains(taEx) == false) {
                // Avoid duplication.
                final XYPlot price_volume_ta = price_volume_ta_map.get(taEx);
                final XYPlot candlestick_ta = candlestick_ta_map.get(taEx);

                final CombinedDomainXYPlot cplot0 = (CombinedDomainXYPlot) this.priceVolumeChart.getPlot();
                final CombinedDomainXYPlot cplot1 = (CombinedDomainXYPlot) this.candlestickChart.getPlot();
                if (price_volume_ta != null)
                    cplot0.add(price_volume_ta, 1); // weight is 1.
                if (candlestick_ta != null)
                    cplot1.add(candlestick_ta, 1); // weight is 1.
                org.yccheok.jstock.charting.Utils.applyChartThemeEx(this.priceVolumeChart);
                org.yccheok.jstock.charting.Utils.applyChartThemeEx(this.candlestickChart);
            }
        } else {
            final CombinedDomainXYPlot cplot0 = (CombinedDomainXYPlot) this.priceVolumeChart.getPlot();
            final CombinedDomainXYPlot cplot1 = (CombinedDomainXYPlot) this.candlestickChart.getPlot();
            final XYPlot price_volume_ta = price_volume_ta_map.get(taEx);
            final XYPlot candlestick_ta = candlestick_ta_map.get(taEx);

            if (price_volume_ta != null)
                cplot0.remove(price_volume_ta);
            if (candlestick_ta != null)
                cplot1.remove(candlestick_ta);
        }

        if (show && this.activeTAExs.contains(taEx) == false) {
            this.activeTAExs.add(taEx);
            JStock.instance().getChartJDialogOptions().add(taEx);
        } else if (!show) {
            this.activeTAExs.remove(taEx);
            JStock.instance().getChartJDialogOptions().remove(taEx);
        }
    }

    private void updateRSI(int days, boolean show) {
        if (this.priceVolumeChart == null) {
            this.priceVolumeChart = this.createPriceVolumeChart(this.priceDataset, this.volumeDataset);
        }
        if (this.candlestickChart == null) {
            this.candlestickChart = this.createCandlestickChart(this.priceOHLCDataset);
        }

        final TAEx taEx = TAEx.newInstance(TA.RSI, new Integer(days));

        if (show) {
            if (price_volume_ta_map.containsKey(taEx) == false) {
                final XYDataset dataset = org.yccheok.jstock.charting.TechnicalAnalysis.createRSI(this.chartDatas,
                        getRSIKey(days), days);
                NumberAxis rangeAxis1 = new NumberAxis(GUIBundle.getString("ChartJDialog_RSI"));
                rangeAxis1.setAutoRangeIncludesZero(false); // override default
                rangeAxis1.setLowerMargin(0.40); // to leave room for volume bars
                DecimalFormat format = new DecimalFormat("0");
                rangeAxis1.setNumberFormatOverride(format);

                final ValueAxis timeAxis = new DateAxis(GUIBundle.getString("ChartJDialog_Date"));
                timeAxis.setLowerMargin(0.02); // reduce the default margins
                timeAxis.setUpperMargin(0.02);

                XYPlot plot = new XYPlot(dataset, timeAxis, rangeAxis1, null);

                XYItemRenderer renderer1 = new XYLineAndShapeRenderer(true, false);
                renderer1.setBaseToolTipGenerator(
                        new StandardXYToolTipGenerator(StandardXYToolTipGenerator.DEFAULT_TOOL_TIP_FORMAT,
                                new SimpleDateFormat("d-MMM-yyyy"), new DecimalFormat("0.00#")));
                plot.setRenderer(0, renderer1);
                price_volume_ta_map.put(taEx, plot);
            }

            if (candlestick_ta_map.containsKey(taEx) == false) {
                try {
                    /* Not sure why. I cannot make priceVolumeChart and candlestickChart sharing the same
                     * plot. If not, this will inhibit incorrect zooming behavior.
                     */
                    candlestick_ta_map.put(taEx, (XYPlot) price_volume_ta_map.get(taEx).clone());
                } catch (CloneNotSupportedException ex) {
                    log.error(null, ex);
                }
            }

            if (this.activeTAExs.contains(taEx) == false) {
                // Avoid duplication.
                final XYPlot price_volume_ta = price_volume_ta_map.get(taEx);
                final XYPlot candlestick_ta = candlestick_ta_map.get(taEx);
                final CombinedDomainXYPlot cplot0 = (CombinedDomainXYPlot) this.priceVolumeChart.getPlot();
                final CombinedDomainXYPlot cplot1 = (CombinedDomainXYPlot) this.candlestickChart.getPlot();

                if (price_volume_ta != null)
                    cplot0.add(price_volume_ta, 1); // weight is 1.
                if (candlestick_ta != null)
                    cplot1.add(candlestick_ta, 1); // weight is 1.
                org.yccheok.jstock.charting.Utils.applyChartThemeEx(this.priceVolumeChart);
                org.yccheok.jstock.charting.Utils.applyChartThemeEx(this.candlestickChart);
            }
        } else {
            final CombinedDomainXYPlot cplot0 = (CombinedDomainXYPlot) this.priceVolumeChart.getPlot();
            final CombinedDomainXYPlot cplot1 = (CombinedDomainXYPlot) this.candlestickChart.getPlot();
            final XYPlot price_volume_ta = price_volume_ta_map.get(taEx);
            final XYPlot candlestick_ta = candlestick_ta_map.get(taEx);

            if (price_volume_ta != null)
                cplot0.remove(price_volume_ta);
            if (candlestick_ta != null)
                cplot1.remove(candlestick_ta);
        }

        if (show && this.activeTAExs.contains(taEx) == false) {
            this.activeTAExs.add(taEx);
            JStock.instance().getChartJDialogOptions().add(taEx);
        } else if (!show) {
            this.activeTAExs.remove(taEx);
            JStock.instance().getChartJDialogOptions().remove(taEx);
        }
    }

    private void showMACDCustomDialog() {
        System.out.println("showMACDCustomDialog");
    }

    // VERY BUGGY CODE STILL! :p
    private void updateMACD(MACD.Period period, boolean show) {
        if (this.priceVolumeChart == null) {
            this.priceVolumeChart = this.createPriceVolumeChart(this.priceDataset, this.volumeDataset);
        }
        if (this.candlestickChart == null) {
            this.candlestickChart = this.createCandlestickChart(this.priceOHLCDataset);
        }

        final TAEx taEx = TAEx.newInstance(TA.MACD, period);
        if (show) {
            if (price_volume_ta_map.containsKey(taEx) == false) {
                final MACD.ChartResult macdChartResult = org.yccheok.jstock.charting.TechnicalAnalysis
                        .createMACD(this.chartDatas, getMACDKey(period), period);

                // MACD!
                NumberAxis rangeAxis1 = new NumberAxis(GUIBundle.getString("ChartJDialog_MACD"));
                rangeAxis1.setAutoRangeIncludesZero(false); // override default
                rangeAxis1.setLowerMargin(0.40); // to leave room for volume bars
                DecimalFormat format = new DecimalFormat("0.00#");
                rangeAxis1.setNumberFormatOverride(format);

                final ValueAxis timeAxis = new DateAxis(GUIBundle.getString("ChartJDialog_Date"));
                timeAxis.setLowerMargin(0.02); // reduce the default margins
                timeAxis.setUpperMargin(0.02);

                XYPlot plot = new XYPlot(macdChartResult.outMACD, timeAxis, rangeAxis1, null);

                XYItemRenderer renderer1 = new XYLineAndShapeRenderer(true, false);
                renderer1.setBaseToolTipGenerator(
                        new StandardXYToolTipGenerator(StandardXYToolTipGenerator.DEFAULT_TOOL_TIP_FORMAT,
                                new SimpleDateFormat("d-MMM-yyyy"), new DecimalFormat("0.00#")));
                plot.setRenderer(0, renderer1);
                org.yccheok.jstock.charting.Utils.setPriceSeriesPaint(renderer1);

                // MACD SIGNAL!
                plot.setDataset(1, macdChartResult.outMACDSignal);
                XYItemRenderer renderer2 = new XYLineAndShapeRenderer(true, false);
                renderer2.setBaseToolTipGenerator(
                        new StandardXYToolTipGenerator(StandardXYToolTipGenerator.DEFAULT_TOOL_TIP_FORMAT,
                                new SimpleDateFormat("d-MMM-yyyy"), new DecimalFormat("0.00#")));
                plot.setRenderer(1, renderer2);

                // VOLUME!
                //plot.setRangeAxis(1, rangeAxis1);
                plot.setDataset(2, macdChartResult.outMACDHist);
                //plot.mapDatasetToRangeAxis(1, 1);

                XYBarRenderer renderer3 = new XYBarRenderer(0.20);

                renderer3.setBaseToolTipGenerator(
                        new StandardXYToolTipGenerator(StandardXYToolTipGenerator.DEFAULT_TOOL_TIP_FORMAT,
                                new SimpleDateFormat("d-MMM-yyyy"), new DecimalFormat("0,000.00")));
                plot.setRenderer(2, renderer3);

                price_volume_ta_map.put(taEx, plot);
            }

            if (candlestick_ta_map.containsKey(taEx) == false) {
                try {
                    /* Not sure why. I cannot make priceVolumeChart and candlestickChart sharing the same
                     * plot. If not, this will inhibit incorrect zooming behavior.
                     */
                    candlestick_ta_map.put(taEx, (XYPlot) price_volume_ta_map.get(taEx).clone());
                } catch (CloneNotSupportedException ex) {
                    log.error(null, ex);
                }
            }

            if (this.activeTAExs.contains(taEx) == false) {
                // Avoid duplication.
                final XYPlot price_volume_ta = price_volume_ta_map.get(taEx);
                final XYPlot candlestick_ta = candlestick_ta_map.get(taEx);

                final CombinedDomainXYPlot cplot0 = (CombinedDomainXYPlot) this.priceVolumeChart.getPlot();
                final CombinedDomainXYPlot cplot1 = (CombinedDomainXYPlot) this.candlestickChart.getPlot();
                if (price_volume_ta != null)
                    cplot0.add(price_volume_ta, 1); // weight is 1.
                if (candlestick_ta != null)
                    cplot1.add(candlestick_ta, 1); // weight is 1.
                org.yccheok.jstock.charting.Utils.applyChartThemeEx(this.priceVolumeChart);
                org.yccheok.jstock.charting.Utils.applyChartThemeEx(this.candlestickChart);
            }
        } else {
            final CombinedDomainXYPlot cplot0 = (CombinedDomainXYPlot) this.priceVolumeChart.getPlot();
            final CombinedDomainXYPlot cplot1 = (CombinedDomainXYPlot) this.candlestickChart.getPlot();
            final XYPlot price_volume_ta = price_volume_ta_map.get(taEx);
            final XYPlot candlestick_ta = candlestick_ta_map.get(taEx);

            if (price_volume_ta != null)
                cplot0.remove(price_volume_ta);
            if (candlestick_ta != null)
                cplot1.remove(candlestick_ta);
        }

        if (show && this.activeTAExs.contains(taEx) == false) {
            this.activeTAExs.add(taEx);
            JStock.instance().getChartJDialogOptions().add(taEx);
        } else if (!show) {
            this.activeTAExs.remove(taEx);
            JStock.instance().getChartJDialogOptions().remove(taEx);
        }
    }

    private void updateEMA(int days, boolean show) {
        if (this.priceVolumeChart == null) {
            this.priceVolumeChart = this.createPriceVolumeChart(this.priceDataset, this.volumeDataset);
        }
        if (this.candlestickChart == null) {
            this.candlestickChart = this.createCandlestickChart(this.priceOHLCDataset);
        }

        final TAEx taEx = TAEx.newInstance(TA.EMA, new Integer(days));
        if (show) {
            TimeSeries timeSeries = null;
            XYDataset dataSet = null;
            final Integer days_integer = days;
            if (false == time_series_exponential_moving_average_map.containsKey(days_integer)) {
                timeSeries = TechnicalAnalysis.createEMA(this.chartDatas, this.getEMAKey(days), days);
                dataSet = new TimeSeriesCollection(timeSeries);
                // Do not put everything into map. We will out of memory.
                if (this.time_series_exponential_moving_average_map.size() < MAX_MAP_SIZE) {
                    time_series_exponential_moving_average_map.put(days_integer, timeSeries);
                }
            } else {
                timeSeries = time_series_exponential_moving_average_map.get(days_integer);
                dataSet = new TimeSeriesCollection(timeSeries);
            }

            {
                if (this.activeTAExs.contains(taEx) == false) {
                    // Avoid duplication.
                    ((TimeSeriesCollection) this.priceDataset).addSeries(timeSeries);
                }
            }
            {
                if (this.activeTAExs.contains(taEx) == false) {
                    // Avoid duplication.
                    final Plot main_plot = (Plot) ((CombinedDomainXYPlot) this.candlestickChart.getPlot())
                            .getSubplots().get(0);
                    final XYPlot plot = (XYPlot) main_plot;
                    final int index = getIndex(taEx, this.xydata_index_map);
                    this.xydata_index_map.put(taEx, index);
                    plot.setDataset(index, dataSet);
                    plot.setRenderer(index, new StandardXYItemRenderer());
                }
            }
        } else {
            {
                final TimeSeries ts = ((TimeSeriesCollection) this.priceDataset).getSeries(this.getEMAKey(days));
                if (ts != null) {
                    ((TimeSeriesCollection) this.priceDataset).removeSeries(ts);
                }
            }
            {
                final Plot main_plot = (Plot) ((CombinedDomainXYPlot) this.candlestickChart.getPlot()).getSubplots()
                        .get(0);
                final XYPlot plot = (XYPlot) main_plot;
                final Integer integer = this.xydata_index_map.get(taEx);
                if (integer != null) {
                    final int index = integer;
                    plot.setDataset(index, null);
                }
            }
        }

        if (show && this.activeTAExs.contains(taEx) == false) {
            this.activeTAExs.add(taEx);
            JStock.instance().getChartJDialogOptions().add(taEx);
        } else if (!show) {
            this.activeTAExs.remove(taEx);
            JStock.instance().getChartJDialogOptions().remove(taEx);
        }
    }

    /**
     * Returns chart change listener, which will be responsible for handling
     * zooming event.
     *
     * @return chart change listener, which will be responsible for handling
     * zooming event
     */
    private ChartChangeListener getChartChangeListner() {
        return new ChartChangeListener() {
            @Override
            public void chartChanged(ChartChangeEvent event) {
                // Is weird. This works well for zoom-in. When we add new CCI or
                // RIS. This event function will be triggered too. However, the
                // returned draw area will always be the old draw area, unless
                // you move your move over.
                // Even I try to capture event.getType() == ChartChangeEventType.NEW_DATASET
                // also doesn't work.
                if (event.getType() == ChartChangeEventType.GENERAL) {
                    ChartJDialog.this.chartLayerUI.updateTraceInfos();
                    // Re-calculating high low value.
                    ChartJDialog.this.updateHighLowJLabels();
                }
            }
        };
    }

    /**
     * Returns the current main plot of this chart dialog.
     *
     * @return the current main plot of this chart dialog
     */
    public XYPlot getPlot() {
        final JFreeChart chart = this.chartPanel.getChart();
        final CombinedDomainXYPlot cplot = (CombinedDomainXYPlot) chart.getPlot();
        final XYPlot plot = (XYPlot) cplot.getSubplots().get(0);
        return plot;
    }

    /**
     * Returns the plot at the specified position in this chart dialog.
     *
     * @param index index of the plot to return
     * @return the plot at the specified position in this chart dialog
     */
    public XYPlot getPlot(int index) {
        final JFreeChart chart = this.chartPanel.getChart();
        final CombinedDomainXYPlot cplot = (CombinedDomainXYPlot) chart.getPlot();
        final XYPlot plot = (XYPlot) cplot.getSubplots().get(index);
        return plot;
    }

    /**
     * Returns the number of plots in this chart dialog.
     *
     * @return the number of plots in this chart dialog
     */
    public int getPlotSize() {
        final JFreeChart chart = this.chartPanel.getChart();
        final CombinedDomainXYPlot cplot = (CombinedDomainXYPlot) chart.getPlot();
        return cplot.getSubplots().size();
    }

    private void updateSMA(int days, boolean show) {
        if (this.priceVolumeChart == null) {
            this.priceVolumeChart = this.createPriceVolumeChart(this.priceDataset, this.volumeDataset);
        }
        if (this.candlestickChart == null) {
            this.candlestickChart = this.createCandlestickChart(this.priceOHLCDataset);
        }

        final TAEx taEx = TAEx.newInstance(TA.SMA, new Integer(days));
        if (show) {
            TimeSeries timeSeries = null;
            XYDataset dataSet = null;
            final Integer days_integer = days;
            if (false == time_series_moving_average_map.containsKey(days_integer)) {
                timeSeries = TechnicalAnalysis.createSMA(this.chartDatas, this.getSMAKey(days), days);
                dataSet = new TimeSeriesCollection(timeSeries);
                // Do not put everything into map. We will out of memory.
                if (this.time_series_moving_average_map.size() < MAX_MAP_SIZE) {
                    time_series_moving_average_map.put(days_integer, timeSeries);
                }
            } else {
                timeSeries = time_series_moving_average_map.get(days_integer);
                dataSet = new TimeSeriesCollection(timeSeries);
            }

            {
                if (this.activeTAExs.contains(taEx) == false) {
                    // Avoid duplication.
                    ((TimeSeriesCollection) this.priceDataset).addSeries(timeSeries);
                }
            }
            {
                if (this.activeTAExs.contains(taEx) == false) {
                    // Avoid duplication.
                    final Plot main_plot = (Plot) ((CombinedDomainXYPlot) this.candlestickChart.getPlot())
                            .getSubplots().get(0);
                    final XYPlot plot = (XYPlot) main_plot;
                    final int index = getIndex(taEx, this.xydata_index_map);
                    this.xydata_index_map.put(taEx, index);
                    plot.setDataset(index, dataSet);
                    plot.setRenderer(index, new StandardXYItemRenderer());
                }
            }
        } else {
            {
                final TimeSeries ts = ((TimeSeriesCollection) this.priceDataset).getSeries(this.getSMAKey(days));
                if (ts != null) {
                    ((TimeSeriesCollection) this.priceDataset).removeSeries(ts);
                }
            }
            {
                final Plot main_plot = (Plot) ((CombinedDomainXYPlot) this.candlestickChart.getPlot()).getSubplots()
                        .get(0);
                final XYPlot plot = (XYPlot) main_plot;
                final Integer integer = this.xydata_index_map.get(taEx);
                if (integer != null) {
                    final int index = integer;
                    plot.setDataset(index, null);
                }
            }
        }

        if (show && this.activeTAExs.contains(taEx) == false) {
            this.activeTAExs.add(taEx);
            JStock.instance().getChartJDialogOptions().add(taEx);
        } else if (!show) {
            this.activeTAExs.remove(taEx);
            JStock.instance().getChartJDialogOptions().remove(taEx);
        }
    }

    public static class TAEx {
        private TAEx(TA ta, Object parameter) {
            if (ta == null || parameter == null) {
                throw new IllegalArgumentException("Method arguments cannot be null");
            }
            this.ta = ta;
            this.parameter = parameter;
        }

        public static TAEx newInstance(TA ta, Object parameter) {
            return new TAEx(ta, parameter);
        }

        /**
         * Get the technical analysis type.
         * @return the technical analysis type
         */
        public TA getTA() {
            return this.ta;
        }

        /**
         * Get the technical analysis parameter
         * @return the technical analysis parameter.
         */
        public Object getParameter() {
            return this.parameter;
        }

        @Override
        public boolean equals(Object o) {
            if (o == this) {
                return true;
            }
            if (!(o instanceof TAEx)) {
                return false;
            }
            TAEx taEx = (TAEx) o;
            return (this.ta == null ? taEx.ta == null : this.ta.equals(taEx.ta))
                    && (this.parameter == null ? taEx.parameter == null : this.parameter.equals(taEx.parameter));
        }

        @Override
        public int hashCode() {
            int hash = 7;
            hash = 53 * hash + (this.ta != null ? this.ta.hashCode() : 0);
            hash = 53 * hash + (this.parameter != null ? this.parameter.hashCode() : 0);
            return hash;
        }

        private final TA ta;
        private final Object parameter;
    }

    private Interval currentInverval = null;
    private Type currentType = null;

    private final StockHistoryServer stockHistoryServer;
    /* Derived based on stockHistoryServer. */
    private List<ChartData> chartDatas;

    private final ChartPanel chartPanel;
    private final Map<Integer, TimeSeries> time_series_moving_average_map = new HashMap<Integer, TimeSeries>();
    private final Map<Integer, TimeSeries> time_series_exponential_moving_average_map = new HashMap<Integer, TimeSeries>();
    /* Days to series index. (For main plot only) */
    private final Map<TAEx, Integer> xydata_index_map = new HashMap<TAEx, Integer>();
    private final List<TAEx> activeTAExs = new ArrayList<TAEx>();
    private static final int MAX_MAP_SIZE = 20;

    private final Map<TAEx, XYPlot> price_volume_ta_map = new HashMap<TAEx, XYPlot>();
    /* Not sure why. I cannot make priceVolumeChart and candlestickChart sharing the same
     * plot. If not, this will inhibit incorrect zooming behavior.
     */
    private final Map<TAEx, XYPlot> candlestick_ta_map = new HashMap<TAEx, XYPlot>();

    private XYDataset priceDataset;
    private XYDataset volumeDataset;
    private TimeSeries priceTimeSeries;
    private OHLCDataset priceOHLCDataset;
    private JFreeChart priceVolumeChart;
    private JFreeChart candlestickChart;

    /**
     * Thread pool, used to hold threads to update high low labels.
     */
    private final Executor updateHighLowLabelsPool = Executors.newFixedThreadPool(1);

    /* Overlay layer. */
    private final ChartLayerUI<ChartPanel> chartLayerUI;

    /* To avoid memory leak. */
    private final java.awt.Frame parent;

    private static final Log log = LogFactory.getLog(ChartJDialog.class);

    // Variables declaration - do not modify//GEN-BEGIN:variables
    private javax.swing.ButtonGroup buttonGroup1;
    private javax.swing.ButtonGroup buttonGroup2;
    private javax.swing.JComboBox jComboBox1;
    private javax.swing.JLabel jLabel1;
    private javax.swing.JLabel jLabel10;
    private javax.swing.JLabel jLabel11;
    private javax.swing.JLabel jLabel12;
    private javax.swing.JLabel jLabel13;
    private javax.swing.JLabel jLabel14;
    private javax.swing.JLabel jLabel2;
    private javax.swing.JLabel jLabel3;
    private javax.swing.JLabel jLabel4;
    private javax.swing.JLabel jLabel5;
    private javax.swing.JLabel jLabel6;
    private javax.swing.JLabel jLabel9;
    private javax.swing.JMenu jMenu1;
    private javax.swing.JMenu jMenu2;
    private javax.swing.JMenu jMenu3;
    private javax.swing.JMenu jMenu4;
    private javax.swing.JMenu jMenu8;
    private javax.swing.JMenuBar jMenuBar1;
    private javax.swing.JMenuItem jMenuItem1;
    private javax.swing.JMenuItem jMenuItem7;
    private javax.swing.JPanel jPanel1;
    private javax.swing.JPanel jPanel2;
    private javax.swing.JPanel jPanel3;
    private javax.swing.JPanel jPanel4;
    private javax.swing.JPanel jPanel5;
    private javax.swing.JPanel jPanel6;
    private javax.swing.JRadioButtonMenuItem jRadioButtonMenuItem1;
    private javax.swing.JRadioButtonMenuItem jRadioButtonMenuItem2;
    private javax.swing.JRadioButtonMenuItem jRadioButtonMenuItem3;
    private javax.swing.JRadioButtonMenuItem jRadioButtonMenuItem4;
    // End of variables declaration//GEN-END:variables

}