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

Java tutorial

Introduction

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

Source

/*
 * JStock - Free Stock Market Software
 * Copyright (C) 2010 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.Color;
import java.awt.Composite;
import java.awt.Font;
import java.awt.FontMetrics;
import java.awt.Graphics2D;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.RenderingHints;
import java.awt.event.KeyEvent;
import java.awt.event.MouseEvent;
import java.awt.geom.Point2D;

import java.awt.geom.Rectangle2D;
import java.text.DecimalFormat;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Calendar;
import java.util.Collections;
import java.util.Date;
import java.util.List;
import javax.swing.SwingUtilities;

import org.jdesktop.jxlayer.JXLayer;
import org.jdesktop.jxlayer.plaf.AbstractLayerUI;
import org.jfree.chart.ChartPanel;
import org.jfree.chart.axis.ValueAxis;
import org.jfree.chart.plot.XYPlot;
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.XYDataset;
import org.jfree.ui.RectangleEdge;
import org.yccheok.jstock.charting.ChartData;
import org.yccheok.jstock.gui.JStockOptions;
import org.yccheok.jstock.gui.JStock;
import org.yccheok.jstock.gui.Utils;
import org.yccheok.jstock.internationalization.GUIBundle;

public class ChartLayerUI<V extends javax.swing.JComponent> extends AbstractLayerUI<V> {

    private static class TraceInfo {
        private final Point2D point;
        private final int dataIndex;
        private final int plotIndex;
        private final int seriesIndex;

        private TraceInfo(Point2D point, int plotIndex, int seriesIndex, int dataIndex) {
            this.point = point;
            this.dataIndex = dataIndex;
            this.plotIndex = plotIndex;
            this.seriesIndex = seriesIndex;
        }

        public static TraceInfo newInstance(Point2D point, int plotIndex, int seriesIndex, int dataIndex) {
            return new TraceInfo(point, plotIndex, seriesIndex, dataIndex);
        }

        /**
         * @return the point
         */
        public Point2D getPoint() {
            return point;
        }

        /**
         * @return the getDataIndex
         */
        public int getDataIndex() {
            return dataIndex;
        }

        /**
         * @return the plotIndex
         */
        public int getPlotIndex() {
            return plotIndex;
        }

        /**
         * @return the seriesIndex
         */
        public int getSeriesIndex() {
            return seriesIndex;
        }
    }

    // Major tracing information for main chart.
    private TraceInfo mainTraceInfo = TraceInfo.newInstance(null, 0, 0, 0);
    // Tracing information for all indicators.
    private final List<TraceInfo> indicatorTraceInfos = new ArrayList<TraceInfo>();

    private final Rectangle2D mainDrawArea = new Rectangle();

    private static final Color COLOR_BLUE = new Color(85, 85, 255);
    private static final Color COLOR_BACKGROUND = new Color(255, 255, 153);
    private static final Color COLOR_BORDER = new Color(255, 204, 0);
    private final ChartJDialog chartJDialog;

    private static final List<String> params = Collections
            .unmodifiableList(Arrays.asList(GUIBundle.getString("StockHistory_Open"),
                    GUIBundle.getString("StockHistory_High"), GUIBundle.getString("StockHistory_Low"),
                    GUIBundle.getString("StockHistory_Close"), GUIBundle.getString("StockHistory_Volume")));

    private static final ThreadLocal<SimpleDateFormat> simpleDataFormatThreadLocal = new ThreadLocal<SimpleDateFormat>() {
        @Override
        protected SimpleDateFormat initialValue() {
            return new SimpleDateFormat("EEE, MMM d, yyyy");
        }
    };

    private static final String longDateString;
    static {
        Calendar c = Calendar.getInstance();
        c.set(2010, 8, 29);
        // Wednesday, September 29, 2010
        longDateString = simpleDataFormatThreadLocal.get().format(c.getTime());
    }

    private int maxWidth = Integer.MIN_VALUE;

    public ChartLayerUI(ChartJDialog chartJDialog) {
        this.chartJDialog = chartJDialog;
    }

    private void drawInformationBox(Graphics2D g2, JXLayer<? extends V> layer) {
        if (JStock.instance().getJStockOptions()
                .getYellowInformationBoxOption() == JStockOptions.YellowInformationBoxOption.Hide) {
            return;
        }

        final Font oldFont = g2.getFont();
        final Font paramFont = oldFont;
        final FontMetrics paramFontMetrics = g2.getFontMetrics(paramFont);
        final Font valueFont = oldFont.deriveFont(oldFont.getStyle() | Font.BOLD, (float) oldFont.getSize() + 1);
        final FontMetrics valueFontMetrics = g2.getFontMetrics(valueFont);
        final Font dateFont = oldFont.deriveFont((float) oldFont.getSize() - 1);
        final FontMetrics dateFontMetrics = g2.getFontMetrics(dateFont);

        final List<ChartData> chartDatas = this.chartJDialog.getChartDatas();
        List<String> values = new ArrayList<String>();
        final ChartData chartData = chartDatas.get(this.mainTraceInfo.getDataIndex());

        // Number formats are generally not synchronized. It is recommended to create separate format instances for each thread. 
        // If multiple threads access a format concurrently, it must be synchronized externally.
        // http://stackoverflow.com/questions/2213410/usage-of-decimalformat-for-the-following-case
        final DecimalFormat integerFormat = new DecimalFormat("###,###");

        // It is common to use OHLC for chat, instead of using PrevPrice.        
        values.add(org.yccheok.jstock.gui.Utils.stockPriceDecimalFormat(chartData.openPrice));
        values.add(org.yccheok.jstock.gui.Utils.stockPriceDecimalFormat(chartData.highPrice));
        values.add(org.yccheok.jstock.gui.Utils.stockPriceDecimalFormat(chartData.lowPrice));
        values.add(org.yccheok.jstock.gui.Utils.stockPriceDecimalFormat(chartData.lastPrice));
        values.add(integerFormat.format(chartData.volume));

        final List<String> indicatorParams = new ArrayList<String>();
        final List<String> indicatorValues = new ArrayList<String>();
        final DecimalFormat decimalFormat = new DecimalFormat("0.00");
        for (TraceInfo indicatorTraceInfo : this.indicatorTraceInfos) {
            final int plotIndex = indicatorTraceInfo.getPlotIndex();
            final int seriesIndex = indicatorTraceInfo.getSeriesIndex();
            final int dataIndex = indicatorTraceInfo.getDataIndex();
            final String name = this.getLegendName(plotIndex, seriesIndex);
            final Number value = this.getValue(plotIndex, seriesIndex, dataIndex);
            if (name == null || value == null) {
                continue;
            }
            indicatorParams.add(name);
            indicatorValues.add(decimalFormat.format(value));
        }

        assert (values.size() == params.size());
        int index = 0;
        final int paramValueWidthMargin = 10;
        final int paramValueHeightMargin = 0;
        // Slightly larger than dateInfoHeightMargin, as font for indicator is
        // larger than date's.
        final int infoIndicatorHeightMargin = 8;
        int maxInfoWidth = -1;
        // paramFontMetrics will always "smaller" than valueFontMetrics.
        int totalInfoHeight = Math.max(paramFontMetrics.getHeight(), valueFontMetrics.getHeight()) * values.size()
                + paramValueHeightMargin * (values.size() - 1);
        for (String param : params) {
            final String value = values.get(index++);
            final int paramStringWidth = paramFontMetrics.stringWidth(param + ":") + paramValueWidthMargin
                    + valueFontMetrics.stringWidth(value);
            if (maxInfoWidth < paramStringWidth) {
                maxInfoWidth = paramStringWidth;
            }
        }

        if (indicatorValues.size() > 0) {
            totalInfoHeight += infoIndicatorHeightMargin;
            totalInfoHeight += Math.max(paramFontMetrics.getHeight(), valueFontMetrics.getHeight())
                    * indicatorValues.size() + paramValueHeightMargin * (indicatorValues.size() - 1);
            index = 0;
            for (String indicatorParam : indicatorParams) {
                final String indicatorValue = indicatorValues.get(index++);
                final int paramStringWidth = paramFontMetrics.stringWidth(indicatorParam + ":")
                        + paramValueWidthMargin + valueFontMetrics.stringWidth(indicatorValue);
                if (maxInfoWidth < paramStringWidth) {
                    maxInfoWidth = paramStringWidth;
                }
            }
        }

        final Date date = new Date(chartData.timestamp);

        // Date formats are not synchronized. It is recommended to create separate format instances for each thread.
        // If multiple threads access a format concurrently, it must be synchronized externally.
        final SimpleDateFormat simpleDateFormat = this.simpleDataFormatThreadLocal.get();
        final String dateString = simpleDateFormat.format(date);
        final int dateStringWidth = dateFontMetrics.stringWidth(dateString);
        final int dateStringHeight = dateFontMetrics.getHeight();
        // We want to avoid information box from keep changing its width while
        // user moves along the mouse. This will prevent user from feeling,
        // information box is flickering, which is uncomfortable to user's eye.
        final int maxStringWidth = Math.max(dateFontMetrics.stringWidth(longDateString),
                Math.max(this.maxWidth, Math.max(dateStringWidth, maxInfoWidth)));
        if (maxStringWidth > this.maxWidth) {
            this.maxWidth = maxStringWidth;
        }
        final int dateInfoHeightMargin = 5;
        final int maxStringHeight = dateStringHeight + dateInfoHeightMargin + totalInfoHeight;

        final int padding = 5;
        final int boxPointMargin = 8;
        final int width = maxStringWidth + (padding << 1);
        final int height = maxStringHeight + (padding << 1);

        /* Get Border Rect Information. */
        /*
        fillRect(1, 1, 1, 1);   // O is rect pixel
            
        xxx
        xOx
        xxx
            
        drawRect(0, 0, 2, 2);   // O is rect pixel
            
        OOO
        OxO
        OOO
         */
        final int borderWidth = width + 2;
        final int borderHeight = height + 2;
        // On left side of the ball.
        final double suggestedBorderX = this.mainTraceInfo.getPoint().getX() - borderWidth - boxPointMargin;
        final double suggestedBorderY = this.mainTraceInfo.getPoint().getY() - (borderHeight >> 1);
        double bestBorderX = 0;
        double bestBorderY = 0;
        if (JStock.instance().getJStockOptions()
                .getYellowInformationBoxOption() == JStockOptions.YellowInformationBoxOption.Stay) {
            if (this.mainTraceInfo.getPoint()
                    .getX() > ((int) (this.mainDrawArea.getX() + this.mainDrawArea.getWidth() + 0.5) >> 1)) {
                bestBorderX = this.mainDrawArea.getX();
                bestBorderY = this.mainDrawArea.getY();
            } else {
                bestBorderX = this.mainDrawArea.getX() + this.mainDrawArea.getWidth() - borderWidth;
                bestBorderY = this.mainDrawArea.getY();
            }
        } else {
            assert (JStock.instance().getJStockOptions()
                    .getYellowInformationBoxOption() == JStockOptions.YellowInformationBoxOption.Follow);
            bestBorderX = suggestedBorderX > this.mainDrawArea.getX()
                    ? (suggestedBorderX + borderWidth) < (this.mainDrawArea.getX() + this.mainDrawArea.getWidth())
                            ? suggestedBorderX
                            : this.mainDrawArea.getX() + this.mainDrawArea.getWidth() - borderWidth - boxPointMargin
                    : this.mainTraceInfo.getPoint().getX() + boxPointMargin;
            bestBorderY = suggestedBorderY > this.mainDrawArea.getY()
                    ? (suggestedBorderY + borderHeight) < (this.mainDrawArea.getY() + this.mainDrawArea.getHeight())
                            ? suggestedBorderY
                            : this.mainDrawArea.getY() + this.mainDrawArea.getHeight() - borderHeight
                                    - boxPointMargin
                    : this.mainDrawArea.getY() + boxPointMargin;
        }

        final double x = bestBorderX + 1;
        final double y = bestBorderY + 1;

        final Object oldValueAntiAlias = g2.getRenderingHint(RenderingHints.KEY_ANTIALIASING);
        final Composite oldComposite = g2.getComposite();
        final Color oldColor = g2.getColor();

        g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
        g2.setColor(COLOR_BORDER);
        g2.drawRoundRect((int) (bestBorderX + 0.5), (int) (bestBorderY + 0.5), borderWidth - 1, borderHeight - 1,
                15, 15);
        g2.setColor(COLOR_BACKGROUND);
        g2.setComposite(Utils.makeComposite(0.75f));
        g2.fillRoundRect((int) (x + 0.5), (int) (y + 0.5), width, height, 15, 15);
        g2.setComposite(oldComposite);
        g2.setColor(oldColor);

        int yy = (int) (y + padding + dateFontMetrics.getAscent() + 0.5);
        g2.setFont(dateFont);
        g2.setColor(COLOR_BLUE);
        g2.drawString(dateString, (int) (((width - dateFontMetrics.stringWidth(dateString)) >> 1) + x + 0.5), yy);

        index = 0;
        yy += dateFontMetrics.getDescent() + dateInfoHeightMargin + valueFontMetrics.getAscent();
        final String CLOSE_STR = GUIBundle.getString("StockHistory_Close");
        for (String param : params) {
            final String value = values.get(index++);
            g2.setColor(Color.BLACK);
            if (param.equals(CLOSE_STR)) {
                // It is common to use OHLC for chat, instead of using PrevPrice.
                final double changePrice = chartData.lastPrice - chartData.openPrice;
                if (changePrice > 0.0) {
                    g2.setColor(JStockOptions.DEFAULT_HIGHER_NUMERICAL_VALUE_FOREGROUND_COLOR);
                } else if (changePrice < 0.0) {
                    g2.setColor(JStockOptions.DEFAULT_LOWER_NUMERICAL_VALUE_FOREGROUND_COLOR);
                }
            }
            g2.setFont(paramFont);
            g2.drawString(param + ":", (int) (padding + x + 0.5), yy);
            g2.setFont(valueFont);
            g2.drawString(value, (int) (width - padding - valueFontMetrics.stringWidth(value) + x + 0.5), yy);
            // Same as yy += valueFontMetrics.getDescent() + paramValueHeightMargin + valueFontMetrics.getAscent()
            yy += paramValueHeightMargin + valueFontMetrics.getHeight();
        }

        g2.setColor(Color.BLACK);
        yy -= paramValueHeightMargin;
        yy += infoIndicatorHeightMargin;

        index = 0;
        for (String indicatorParam : indicatorParams) {
            final String indicatorValue = indicatorValues.get(index++);
            g2.setFont(paramFont);
            g2.drawString(indicatorParam + ":", (int) (padding + x + 0.5), yy);
            g2.setFont(valueFont);
            g2.drawString(indicatorValue,
                    (int) (width - padding - valueFontMetrics.stringWidth(indicatorValue) + x + 0.5), yy);
            // Same as yy += valueFontMetrics.getDescent() + paramValueHeightMargin + valueFontMetrics.getAscent()
            yy += paramValueHeightMargin + valueFontMetrics.getHeight();
        }

        g2.setColor(oldColor);
        g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, oldValueAntiAlias);
        g2.setFont(oldFont);
    }

    @Override
    protected void paintLayer(Graphics2D g2, JXLayer<? extends V> layer) {
        super.paintLayer(g2, layer);

        if (this.mainTraceInfo.getPoint() == null) {
            return;
        }

        final Object oldValueAntiAlias = g2.getRenderingHint(RenderingHints.KEY_ANTIALIASING);
        final Color oldColor = g2.getColor();

        g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
        g2.setColor(COLOR_BLUE);
        final int BALL_RADIUS = 8;
        g2.fillOval((int) (this.mainTraceInfo.getPoint().getX() - (BALL_RADIUS >> 1) + 0.5),
                (int) (this.mainTraceInfo.getPoint().getY() - (BALL_RADIUS >> 1) + 0.5), BALL_RADIUS, BALL_RADIUS);

        for (TraceInfo indicatorTraceInfo : this.indicatorTraceInfos) {
            final Point2D point = indicatorTraceInfo.getPoint();
            if (null == point) {
                continue;
            }
            g2.fillOval((int) (point.getX() - (BALL_RADIUS >> 1) + 0.5),
                    (int) (point.getY() - (BALL_RADIUS >> 1) + 0.5), BALL_RADIUS, BALL_RADIUS);
        }

        g2.setColor(oldColor);

        g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, oldValueAntiAlias);
        this.drawInformationBox(g2, layer);
    }

    @Override
    protected void processMouseEvent(MouseEvent e, JXLayer<? extends V> layer) {
        super.processMouseEvent(e, layer);
        this.processEvent(e, layer);
    }

    @Override
    protected void processMouseMotionEvent(MouseEvent e, JXLayer<? extends V> layer) {
        super.processMouseMotionEvent(e, layer);
        this.processEvent(e, layer);
    }

    // Indicator tracing information will always be determined by main tracing
    // information. Not by mouse coordinate. Their x-location, must be exactly
    // same as mainTraceInfo's point x-location.
    //
    // Their behavior are diference from mainTraceInfo. For mainTraceInfo, if we
    // move our mouse cursor to a location where ball cannot be drawn, previous
    // ball will remain in its old location.
    //
    // However, in order to avoid confusion from users, "why CCI ball is not 
    // x-location tally with main ball", if an indicator ball unable to be drawn
    // x-parallel with main ball, it will be removed.
    //
    // If particular indicator point is not within mainDrawArea, it will be removed
    // immediately. Their index, however will be updated, in order to access
    // time series data, which will be shown in information box.
    private void updateIndicatorTraceInfos(int mainPointIndex) {
        this.indicatorTraceInfos.clear();
        if (this.mainTraceInfo == null) {
            return;
        }

        final ChartPanel chartPanel = this.chartJDialog.getChartPanel();

        Day day = null;

        final XYDataset xyDataset = this.chartJDialog.getPlot().getDataset();
        if (xyDataset instanceof TimeSeriesCollection) {
            // Get the date.
            day = (Day) ((TimeSeriesCollection) xyDataset).getSeries(0).getDataItem(mainPointIndex).getPeriod();

            // 0 means main plot.
            final XYPlot plot = this.chartJDialog.getPlot();
            final TimeSeriesCollection timeSeriesCollection = (TimeSeriesCollection) plot.getDataset();
            // Start with 1. We are not interested in main series.
            for (int j = 1, size = timeSeriesCollection.getSeriesCount(); j < size; j++) {
                final TimeSeries timeSeries = timeSeriesCollection.getSeries(j);
                /* Time consuming. */
                final int dataIndex = getDataIndex(timeSeries, day);

                if (dataIndex < 0) {
                    continue;
                }

                final Point2D point = this.getPoint(0, j, dataIndex);
                final String name = this.getLegendName(0, j);
                final Number value = this.getValue(0, j, dataIndex);

                if (point == null || name == null || value == null) {
                    continue;
                }

                // We will never draw ball for SMA, EMA...
                this.indicatorTraceInfos.add(TraceInfo.newInstance(null, 0, j, dataIndex));
            }
        } else {
            final Date date = ((org.jfree.data.xy.DefaultHighLowDataset) xyDataset).getXDate(0, mainPointIndex);
            // OK to do so? Is "day" only used to compare day, excluding time information?
            // Will day 13th September 2009, 1:00pm same as another day 13th September 2009, 3:00pm?
            day = new Day(date);

            // 0 means main plot.
            final XYPlot plot = this.chartJDialog.getPlot();
            final int count = plot.getDatasetCount();
            for (int i = 1; i < count; i++) {
                final TimeSeriesCollection timeSeriesCollection = (TimeSeriesCollection) plot.getDataset(i);

                /* Not ready. */
                if (timeSeriesCollection == null) {
                    continue;
                }

                final TimeSeries timeSeries = timeSeriesCollection.getSeries(0);
                /* Time consuming. */
                final int dataIndex = getDataIndex(timeSeries, day);

                if (dataIndex < 0) {
                    continue;
                }

                final Point2D point = this.getPoint(0, i, dataIndex);
                final String name = this.getLegendName(0, i);
                final Number value = this.getValue(0, i, dataIndex);

                if (point == null || name == null || value == null) {
                    continue;
                }

                // We will never draw ball for SMA, EMA...
                this.indicatorTraceInfos.add(TraceInfo.newInstance(null, 0, i, dataIndex));
            }
        }

        // Begin with 1. 0 is main plot.
        for (int i = 1, size = this.chartJDialog.getPlotSize(); i < size; i++) {
            final XYPlot plot = this.chartJDialog.getPlot(i);
            final TimeSeriesCollection timeSeriesCollection = (TimeSeriesCollection) plot.getDataset();
            // So far, for subplot, each of them only have 1 series.
            assert (1 == timeSeriesCollection.getSeriesCount());
            for (int j = 0, size2 = timeSeriesCollection.getSeriesCount(); j < size2; j++) {
                final TimeSeries timeSeries = timeSeriesCollection.getSeries(j);
                /* Time consuming. */
                final int dataIndex = getDataIndex(timeSeries, day);

                if (dataIndex < 0) {
                    continue;
                }

                final Point2D point = this.getPoint(i, j, dataIndex);
                final String name = this.getLegendName(i, j);
                final Number value = this.getValue(i, j, dataIndex);

                if (point == null || name == null || value == null) {
                    continue;
                }

                final Rectangle2D plotArea = chartPanel.getChartRenderingInfo().getPlotInfo().getSubplotInfo(i)
                        .getDataArea();
                if (plotArea.contains(point)) {
                    this.indicatorTraceInfos.add(TraceInfo.newInstance(point, i, j, dataIndex));
                } else {
                    this.indicatorTraceInfos.add(TraceInfo.newInstance(null, i, j, dataIndex));
                }
            }
        }
    }

    private TimeSeries getTimeSeries(int plotIndex, int seriesIndex) {
        final ChartPanel chartPanel = this.chartJDialog.getChartPanel();

        if (plotIndex >= chartPanel.getChartRenderingInfo().getPlotInfo().getSubplotCount()) {
            /* Not ready yet. */
            return null;
        }

        if (plotIndex >= this.chartJDialog.getPlotSize()) {
            /* Not ready yet. */
            return null;
        }

        final XYDataset xyDataset = this.chartJDialog.getPlot(plotIndex).getDataset();
        if (xyDataset instanceof TimeSeriesCollection) {
            final TimeSeriesCollection timeSeriesCollection = (TimeSeriesCollection) xyDataset;
            if (seriesIndex >= timeSeriesCollection.getSeriesCount()) {
                return null;
            }
            return timeSeriesCollection.getSeries(seriesIndex);
        } else {
            // 0 is candlestick chart.
            if (plotIndex == 0 && seriesIndex == 0) {
                return null;
            }
            if (seriesIndex >= this.chartJDialog.getPlot(plotIndex).getDatasetCount()) {
                return null;
            }
            final XYDataset d = this.chartJDialog.getPlot(plotIndex).getDataset(seriesIndex);
            return ((TimeSeriesCollection) d).getSeries(0);
        }
    }

    // Possible null, if the chart had been updated, but the drawing is not being
    // rendered properly yet.
    private String getLegendName(int plotIndex, int seriesIndex) {
        final TimeSeries timeSeries = this.getTimeSeries(plotIndex, seriesIndex);
        if (timeSeries == null) {
            return null;
        }
        return (String) timeSeries.getKey();
    }

    // Possible null, if the chart had been updated, but the drawing is not being
    // rendered properly yet.
    // (We need to refactor this method to merge with getPoint)
    private Point2D.Double _getPointForCandlestick(int plotIndex, int seriesIndex, int dataIndex) {
        final ChartPanel chartPanel = this.chartJDialog.getChartPanel();
        final XYPlot plot = this.chartJDialog.getPlot(plotIndex);
        final ValueAxis domainAxis = plot.getDomainAxis();
        final RectangleEdge domainAxisEdge = plot.getDomainAxisEdge();
        final ValueAxis rangeAxis = plot.getRangeAxis();
        final RectangleEdge rangeAxisEdge = plot.getRangeAxisEdge();

        final org.jfree.data.xy.DefaultHighLowDataset defaultHighLowDataset = (org.jfree.data.xy.DefaultHighLowDataset) plot
                .getDataset(seriesIndex);

        if (dataIndex >= defaultHighLowDataset.getItemCount(0)) {
            /* Not ready yet. */
            return null;
        }

        final double xValue = defaultHighLowDataset.getXDate(0, dataIndex).getTime();
        final double yValue = defaultHighLowDataset.getCloseValue(0, dataIndex);
        final Rectangle2D plotArea = chartPanel.getChartRenderingInfo().getPlotInfo().getSubplotInfo(plotIndex)
                .getDataArea();
        final double xJava2D = domainAxis.valueToJava2D(xValue, plotArea, domainAxisEdge);
        final double yJava2D = rangeAxis.valueToJava2D(yValue, plotArea, rangeAxisEdge);
        // Use Double version, to avoid from losing precision.
        return new Point2D.Double(xJava2D, yJava2D);
    }

    // Possible null, if the chart had been updated, but the drawing is not being
    // rendered properly yet.
    private Point2D.Double getPoint(int plotIndex, int seriesIndex, int dataIndex) {
        final TimeSeries timeSeries = this.getTimeSeries(plotIndex, seriesIndex);
        if (timeSeries == null) {
            // Possible this is candlestick?
            final XYPlot plot = this.chartJDialog.getPlot(plotIndex);
            if (plot.getDataset(seriesIndex) instanceof org.jfree.data.xy.DefaultHighLowDataset) {
                return this._getPointForCandlestick(plotIndex, seriesIndex, dataIndex);
            }
            return null;
        }

        if (dataIndex >= timeSeries.getItemCount()) {
            /* Not ready yet. */
            return null;
        }

        final ChartPanel chartPanel = this.chartJDialog.getChartPanel();
        final XYPlot plot = this.chartJDialog.getPlot(plotIndex);
        final ValueAxis domainAxis = plot.getDomainAxis();
        final RectangleEdge domainAxisEdge = plot.getDomainAxisEdge();
        final ValueAxis rangeAxis = plot.getRangeAxis();
        final RectangleEdge rangeAxisEdge = plot.getRangeAxisEdge();

        final TimeSeriesDataItem timeSeriesDataItem = timeSeries.getDataItem(dataIndex);
        final double xValue = timeSeriesDataItem.getPeriod().getFirstMillisecond();
        final double yValue = timeSeriesDataItem.getValue().doubleValue();
        final Rectangle2D plotArea = chartPanel.getChartRenderingInfo().getPlotInfo().getSubplotInfo(plotIndex)
                .getDataArea();
        final double xJava2D = domainAxis.valueToJava2D(xValue, plotArea, domainAxisEdge);
        final double yJava2D = rangeAxis.valueToJava2D(yValue, plotArea, rangeAxisEdge);
        // Use Double version, to avoid from losing precision.
        return new Point2D.Double(xJava2D, yJava2D);
    }

    // Possible null, if the chart had been updated, but the drawing is not being
    // rendered properly yet.
    private Number getValue(int plotIndex, int seriesIndex, int dataIndex) {
        final TimeSeries timeSeries = this.getTimeSeries(plotIndex, seriesIndex);
        if (timeSeries == null) {
            return null;
        }

        if (dataIndex >= timeSeries.getItemCount()) {
            /* Not ready yet. */
            return null;
        }

        return timeSeries.getDataItem(dataIndex).getValue();
    }

    // Use binary search to search for index.
    // Return -1 if failed.
    private int getDataIndex(TimeSeries timeSeries, Day targetDay) {
        int low = 0;
        int high = timeSeries.getItemCount() - 1;

        while (low <= high) {
            int mid = (low + high) >>> 1;

            final TimeSeriesDataItem timeSeriesDataItem = timeSeries.getDataItem(mid);
            final Day searchDay = (Day) timeSeriesDataItem.getPeriod();
            final long cmp = searchDay.compareTo(targetDay);

            if (cmp < 0) {
                low = mid + 1;
            } else if (cmp > 0) {
                high = mid - 1;
            } else {
                return mid;
            }
        }
        return -1;
    }

    // Use to move yellow information box by using arrow key.
    private void updateTraceInfosIfPossible(int dataOffset) {
        // Do we have a valid mainTraceInfo?
        if (this.mainTraceInfo.point == null) {
            // Nope. Returns early.
            return;
        }
        final int newDataIndex = dataOffset + this.mainTraceInfo.dataIndex;
        if (newDataIndex < 0) {
            // We try to offset beyond chart boundary. Returns early.
        }

        /* Try to get correct main chart area. */
        final ChartPanel chartPanel = this.chartJDialog.getChartPanel();
        final Rectangle2D _plotArea = chartPanel.getChartRenderingInfo().getPlotInfo().getSubplotInfo(0)
                .getDataArea();

        final Point2D tmpPoint = this.getPoint(0, 0, newDataIndex);
        if (tmpPoint == null || _plotArea.contains(tmpPoint) == false) {
            return;
        }

        this.mainTraceInfo = TraceInfo.newInstance(tmpPoint, 0, 0, newDataIndex);
        this.updateTraceInfos();
    }

    public void updateTraceInfos() {
        this.updateMainTraceInfo();
        // updateIndicatorTraceInfos must always be called only after updateMainTraceInfo.
        this.updateIndicatorTraceInfos(this.mainTraceInfo.getDataIndex());
        this.setDirty(true);
    }

    private void updateMainTraceInfo() {
        if (this.updateMainTraceInfo(this.mainTraceInfo.getPoint()) == false) {
            /* Clear the mainTraceInfo. */
            this.mainTraceInfo = TraceInfo.newInstance(null, 0, 0, 0);
        }
    }

    // Update this.mainDrawArea, this.mainTraceInfo
    // If traceInfo is not within this.mainDrawArea, this.mainTraceInfo will not be
    // updated. However, this.mainDrawArea will always be updated.
    // (We need to refactor this method to merge with updateMainTraceInfo)
    private boolean _updateMainTraceInfoForCandlestick(Point2D point) {
        if (point == null) {
            return false;
        }

        final ChartPanel chartPanel = this.chartJDialog.getChartPanel();
        // Top most plot.
        final XYPlot plot = this.chartJDialog.getPlot();

        final org.jfree.data.xy.DefaultHighLowDataset defaultHighLowDataset = (org.jfree.data.xy.DefaultHighLowDataset) plot
                .getDataset();

        // I also not sure why. This is what are being done in Mouse Listener Demo 4.
        //
        // Don't use it. It will cause us to lose precision.
        //final Point2D p2 = chartPanel.translateScreenToJava2D((Point)point);

        /* Try to get correct main chart area. */
        final Rectangle2D _plotArea = chartPanel.getChartRenderingInfo().getPlotInfo().getSubplotInfo(0)
                .getDataArea();

        final ValueAxis domainAxis = plot.getDomainAxis();
        final RectangleEdge domainAxisEdge = plot.getDomainAxisEdge();
        final ValueAxis rangeAxis = plot.getRangeAxis();
        final RectangleEdge rangeAxisEdge = plot.getRangeAxisEdge();
        // Don't use it. It will cause us to lose precision.
        //final double coordinateX = domainAxis.java2DToValue(p2.getX(), _plotArea,
        //        domainAxisEdge);
        final double coordinateX = domainAxis.java2DToValue(point.getX(), _plotArea, domainAxisEdge);
        //double coordinateY = rangeAxis.java2DToValue(mousePoint2.getY(), plotArea,
        //        rangeAxisEdge);

        int low = 0;
        int high = defaultHighLowDataset.getItemCount(0) - 1;
        Date date = new Date((long) coordinateX);
        final long time = date.getTime();
        long bestDistance = Long.MAX_VALUE;
        int bestMid = 0;

        while (low <= high) {
            int mid = (low + high) >>> 1;

            final Date d = defaultHighLowDataset.getXDate(0, mid);
            final long search = d.getTime();
            final long cmp = search - time;

            if (cmp < 0) {
                low = mid + 1;
            } else if (cmp > 0) {
                high = mid - 1;
            } else {
                bestDistance = 0;
                bestMid = mid;
                break;
            }

            final long abs_cmp = Math.abs(cmp);
            if (abs_cmp < bestDistance) {
                bestDistance = abs_cmp;
                bestMid = mid;
            }
        }

        final double xValue = defaultHighLowDataset.getXDate(0, bestMid).getTime();
        final double yValue = defaultHighLowDataset.getCloseValue(0, bestMid);
        final double xJava2D = domainAxis.valueToJava2D(xValue, _plotArea, domainAxisEdge);
        final double yJava2D = rangeAxis.valueToJava2D(yValue, _plotArea, rangeAxisEdge);

        final int tmpIndex = bestMid;
        // translateJava2DToScreen will internally convert Point2D.Double to Point.
        final Point2D tmpPoint = chartPanel.translateJava2DToScreen(new Point2D.Double(xJava2D, yJava2D));
        this.mainDrawArea.setRect(_plotArea);

        if (this.mainDrawArea.contains(tmpPoint)) {
            // 0 indicates main plot.
            this.mainTraceInfo = TraceInfo.newInstance(tmpPoint, 0, 0, tmpIndex);
            return true;
        }
        return false;
    }

    @Override
    public void processKeyEvent(java.awt.event.KeyEvent e, JXLayer<? extends V> l) {
        if (e.getID() != KeyEvent.KEY_PRESSED) {
            // We are only interested in KEY_PRESSED event.
            return;
        }
        final int code = e.getKeyCode();
        switch (code) {
        case KeyEvent.VK_LEFT:
            this.updateTraceInfosIfPossible(-1);
            break;
        case KeyEvent.VK_RIGHT:
            this.updateTraceInfosIfPossible(+1);
            break;
        }
    }

    // Update this.mainDrawArea, this.mainTraceInfo
    // If traceInfo is not within this.mainDrawArea, this.mainTraceInfo will not be
    // updated. However, this.mainDrawArea will always be updated.
    private boolean updateMainTraceInfo(Point2D point) {
        if (point == null) {
            return false;
        }

        final ChartPanel chartPanel = this.chartJDialog.getChartPanel();
        // Top most plot.
        final XYPlot plot = this.chartJDialog.getPlot();

        if (plot.getDataset() instanceof org.jfree.data.xy.DefaultHighLowDataset) {
            return this._updateMainTraceInfoForCandlestick(point);
        }

        final TimeSeriesCollection timeSeriesCollection = (TimeSeriesCollection) plot.getDataset();
        // 0 are the main chart. 1, 2, 3... are TA.
        final TimeSeries timeSeries = timeSeriesCollection.getSeries(0);

        // I also not sure why. This is what are being done in Mouse Listener Demo 4.
        //
        // Don't use it. It will cause us to lose precision.
        //final Point2D p2 = chartPanel.translateScreenToJava2D((Point)point);

        /* Try to get correct main chart area. */
        final Rectangle2D _plotArea = chartPanel.getChartRenderingInfo().getPlotInfo().getSubplotInfo(0)
                .getDataArea();

        final ValueAxis domainAxis = plot.getDomainAxis();
        final RectangleEdge domainAxisEdge = plot.getDomainAxisEdge();
        final ValueAxis rangeAxis = plot.getRangeAxis();
        final RectangleEdge rangeAxisEdge = plot.getRangeAxisEdge();
        // Don't use it. It will cause us to lose precision.
        //final double coordinateX = domainAxis.java2DToValue(p2.getX(), _plotArea,
        //        domainAxisEdge);
        final double coordinateX = domainAxis.java2DToValue(point.getX(), _plotArea, domainAxisEdge);
        //double coordinateY = rangeAxis.java2DToValue(mousePoint2.getY(), plotArea,
        //        rangeAxisEdge);

        int low = 0;
        int high = timeSeries.getItemCount() - 1;
        Date date = new Date((long) coordinateX);
        final long time = date.getTime();
        long bestDistance = Long.MAX_VALUE;
        int bestMid = 0;

        while (low <= high) {
            int mid = (low + high) >>> 1;

            final TimeSeriesDataItem timeSeriesDataItem = timeSeries.getDataItem(mid);
            final Day day = (Day) timeSeriesDataItem.getPeriod();
            final long search = day.getFirstMillisecond();
            final long cmp = search - time;

            if (cmp < 0) {
                low = mid + 1;
            } else if (cmp > 0) {
                high = mid - 1;
            } else {
                bestDistance = 0;
                bestMid = mid;
                break;
            }

            final long abs_cmp = Math.abs(cmp);
            if (abs_cmp < bestDistance) {
                bestDistance = abs_cmp;
                bestMid = mid;
            }
        }

        final TimeSeriesDataItem timeSeriesDataItem = timeSeries.getDataItem(bestMid);
        final double xValue = timeSeriesDataItem.getPeriod().getFirstMillisecond();
        final double yValue = timeSeriesDataItem.getValue().doubleValue();
        final double xJava2D = domainAxis.valueToJava2D(xValue, _plotArea, domainAxisEdge);
        final double yJava2D = rangeAxis.valueToJava2D(yValue, _plotArea, rangeAxisEdge);

        final int tmpIndex = bestMid;
        // translateJava2DToScreen will internally convert Point2D.Double to Point.
        final Point2D tmpPoint = chartPanel.translateJava2DToScreen(new Point2D.Double(xJava2D, yJava2D));
        this.mainDrawArea.setRect(_plotArea);

        if (this.mainDrawArea.contains(tmpPoint)) {
            // 0 indicates main plot.
            this.mainTraceInfo = TraceInfo.newInstance(tmpPoint, 0, 0, tmpIndex);
            return true;
        }
        return false;
    }

    private void processEvent(MouseEvent e, JXLayer layer) {
        if (MouseEvent.MOUSE_DRAGGED == e.getID()) {
            return;
        }

        final Point mousePoint = SwingUtilities.convertPoint(e.getComponent(), e.getPoint(), layer);
        if (this.updateMainTraceInfo(mousePoint)) {
            this.updateIndicatorTraceInfos(this.mainTraceInfo.getDataIndex());
            this.setDirty(true);
        }
    }
}