net.tourbook.chart.ChartComponents.java Source code

Java tutorial

Introduction

Here is the source code for net.tourbook.chart.ChartComponents.java

Source

/*******************************************************************************
 * Copyright (C) 2005, 2013  Wolfgang Schramm and Contributors
 * 
 * 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 version 2 of the License.
 * 
 * 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 St, Fifth Floor, Boston, MA 02110, USA
 *******************************************************************************/
package net.tourbook.chart;

import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.GregorianCalendar;

import net.tourbook.common.UI;
import net.tourbook.common.util.Util;

import org.eclipse.swt.SWT;
import org.eclipse.swt.events.ControlAdapter;
import org.eclipse.swt.events.ControlEvent;
import org.eclipse.swt.graphics.GC;
import org.eclipse.swt.graphics.Rectangle;
import org.eclipse.swt.layout.GridData;
import org.eclipse.swt.layout.GridLayout;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Display;
import org.eclipse.swt.widgets.Event;
import org.joda.time.DateTime;
import org.joda.time.format.DateTimeFormat;
import org.joda.time.format.DateTimeFormatter;

/**
 * Chart widget which represents the chart ui The chart consists of these components
 * <p>
 * The chart widget consists has the following heights:
 * 
 * <pre>
 *  {@link #_devMarginTop}
 *  {@link #_devXTitleBarHeight}
 * 
 *  |devSliderBarHeight
 *  |#graph#
 * 
 *   verticalDistance
 * 
 *  |devSliderBarHeight
 *  |#graph#
 * 
 *   verticalDistance
 * 
 *     ...
 * 
 *   |devSliderBarHeight
 *   |#graph#
 * 
 *   {@link #_devXAxisHeight}
 * </pre>
 */
public class ChartComponents extends Composite {

    public static final int BAR_SELECTION_DELAY_TIME = 100;

    /**
     * min/max pixel widthDev/heightDev of the chart
     */
    static final int CHART_MIN_WIDTH = 5;
    static final int CHART_MIN_HEIGHT = 5;

    //   static final int            CHART_MAX_WIDTH            = Integer.MAX_VALUE;            // 2'147'483'647
    //   static final int            CHART_MAX_WIDTH            = 1000000000;                  // 1'000'000'000
    static final long CHART_MAX_WIDTH = 1000000000000L; // 1'000'000'000'000
    //                                                                           //   308'333'095
    static final int CHART_MAX_HEIGHT = 10000;

    static final int SLIDER_BAR_HEIGHT = 10;
    static final int TITLE_BAR_HEIGHT = 18; //15;
    static final int MARGIN_TOP_WITH_TITLE = 5;
    static final int MARGIN_TOP_WITHOUT_TITLE = 10;

    /**
     * Number of seconds in one day.
     */
    private static final int DAY_IN_SECONDS = 24 * 60 * 60;

    private static final int YEAR_IN_SECONDS = 366 * DAY_IN_SECONDS;

    private static final int MONTH_IN_SECONDS = 31 * DAY_IN_SECONDS;

    private final Chart _chart;

    /**
     * top margin of the chart (and all it's components)
     */
    private int _devMarginTop = MARGIN_TOP_WITHOUT_TITLE;

    /**
     * height of the slider bar, 0 indicates that the slider is not visible
     */
    int _devSliderBarHeight = 0;

    /**
     * height of the title bar, 0 indicates that the title is not visible
     */
    private int _devXTitleBarHeight = 0;

    /**
     * height of the horizontal axis
     */
    private final int _devXAxisHeight = 25;

    /**
     * width of the vertical axis
     */
    private final int _yAxisWidthLeft = 50;
    private int _yAxisWidthLeftWithTitle = _yAxisWidthLeft;
    private final int _yAxisWidthRight = 50;

    /**
     * vertical distance between two graphs
     */
    private final int _chartsVerticalDistance = 15;

    /**
     * contains the {@link SynchConfiguration} for the current chart and will be used from the chart
     * which is synchronized
     */
    SynchConfiguration _synchConfigOut = null;

    /**
     * when a {@link SynchConfiguration} is set, this chart will be synchronized with the chart
     * which set's the synch config
     */
    SynchConfiguration _synchConfigSrc = null;

    /**
     * visible chart rectangle
     */
    private Rectangle _visibleGraphRect;

    final ChartComponentGraph componentGraph;
    final ChartComponentAxis componentAxisLeft;
    final ChartComponentAxis componentAxisRight;

    private ChartDataModel _chartDataModel = null;

    private ChartDrawingData _chartDrawingData;

    public boolean _useAdvancedGraphics = true;

    private static final String _monthLabels[] = { Messages.Month_jan, Messages.Month_feb, Messages.Month_mar,
            Messages.Month_apr, Messages.Month_mai, Messages.Month_jun, Messages.Month_jul, Messages.Month_aug,
            Messages.Month_sep, Messages.Month_oct, Messages.Month_nov, Messages.Month_dec };

    private static final String _monthShortLabels[] = { Integer.toString(1), Integer.toString(2),
            Integer.toString(3), Integer.toString(4), Integer.toString(5), Integer.toString(6), Integer.toString(7),
            Integer.toString(8), Integer.toString(9), Integer.toString(10), Integer.toString(11),
            Integer.toString(12) };

    /**
     * Width in pixel for all months in one year
     */
    private int _devAllMonthLabelWidth = -1;
    private int _devAllMonthShortLabelWidth = -1;
    private int _devYearLabelWidth;

    private final int[] _keyDownCounter = new int[1];
    private final int[] _lastKeyDownCounter = new int[1];

    private final Calendar _calendar = GregorianCalendar.getInstance();

    /**
     * this error message is displayed instead of the chart when it's not <code>null</code>
     */
    String errorMessage;

    private final DateTimeFormatter _dtFormatter = DateTimeFormat.forStyle("M-"); //$NON-NLS-1$

    private long _historyUnitStart;
    private long _historyUnitDuration;

    private int[] _historyYears;

    /**
     * Contains number of days for each month
     */
    private int[][] _historyMonths;
    private int[] _historyDOY;

    /**
     * Create and layout the components of the chart
     * 
     * @param parent
     * @param style
     */
    ChartComponents(final Chart parent, final int style) {

        super(parent, style);

        GridData gd;
        _chart = parent;

        // set layout for the components
        gd = new GridData(SWT.FILL, SWT.FILL, true, true);
        setLayoutData(gd);

        // set layout for this chart
        final GridLayout gl = new GridLayout(3, false);
        gl.horizontalSpacing = 0;
        gl.verticalSpacing = 0;
        gl.marginWidth = 0;
        gl.marginHeight = 0;
        setLayout(gl);

        // left: create left axis canvas
        componentAxisLeft = new ChartComponentAxis(parent, this, SWT.NONE);
        gd = new GridData(SWT.NONE, SWT.FILL, false, true);
        gd.widthHint = _yAxisWidthLeft;
        componentAxisLeft.setLayoutData(gd);

        // center: create chart canvas
        componentGraph = new ChartComponentGraph(parent, this, SWT.NONE);
        gd = new GridData(SWT.FILL, SWT.FILL, true, true);
        componentGraph.setLayoutData(gd);

        // right: create right axis canvas
        componentAxisRight = new ChartComponentAxis(parent, this, SWT.NONE);
        gd = new GridData(SWT.NONE, SWT.FILL, false, true);
        gd.widthHint = _yAxisWidthRight;
        componentAxisRight.setLayoutData(gd);

        componentAxisLeft.setComponentGraph(componentGraph);
        componentAxisRight.setComponentGraph(componentGraph);

        addListener();

        getMonthLabelWidth();
    }

    private void addListener() {

        // this is the only resize listener for the whole chart
        addControlListener(new ControlAdapter() {
            @Override
            public void controlResized(final ControlEvent event) {
                onResize();
            }
        });
    }

    /**
     * Computes all the data for the chart
     * 
     * @return chart drawing data
     */
    private ChartDrawingData createDrawingData() {

        // compute the graphs and axis
        final ArrayList<GraphDrawingData> graphDrawingData = new ArrayList<GraphDrawingData>();

        final ChartDrawingData chartDrawingData = new ChartDrawingData(graphDrawingData);

        chartDrawingData.chartDataModel = _chartDataModel;

        final ArrayList<ChartDataYSerie> yDataList = _chartDataModel.getYData();
        final ChartDataXSerie xData = _chartDataModel.getXData();
        final ChartDataXSerie xData2nd = _chartDataModel.getXData2nd();

        final int graphCount = yDataList.size();
        int graphIndex = 1;

        // loop all graphs
        for (final ChartDataYSerie yData : yDataList) {

            final GraphDrawingData drawingData = new GraphDrawingData(chartDrawingData, yData.getChartType());

            graphDrawingData.add(drawingData);

            // set chart title above the first graph
            if (graphIndex == 1) {

                drawingData.setXTitle(_chartDataModel.getTitle());

                // set the chart title height and margin
                final String title = drawingData.getXTitle();
                final ChartSegments chartSegments = xData.getChartSegments();

                if (title != null && title.length() > 0 || //
                        (chartSegments != null && chartSegments.segmentTitle != null)) {

                    _devXTitleBarHeight = TITLE_BAR_HEIGHT;
                    _devMarginTop = MARGIN_TOP_WITH_TITLE;
                }
            }

            // set x/y data
            drawingData.setXData(xData);
            drawingData.setXData2nd(xData2nd);
            drawingData.setYData(yData);

            // compute x/y values
            createDrawingData_X(drawingData);
            createDrawingData_Y(drawingData, graphCount, graphIndex);

            // reset adjusted y-slider value
            yData.adjustedYValue = Float.MIN_VALUE;

            graphIndex++;
        }

        // set values after they have been computed
        chartDrawingData.devMarginTop = _devMarginTop;
        chartDrawingData.devXTitelBarHeight = _devXTitleBarHeight;
        chartDrawingData.devSliderBarHeight = _devSliderBarHeight;
        chartDrawingData.devXAxisHeight = _devXAxisHeight;
        chartDrawingData.devDevVisibleChartWidth = getDevVisibleChartWidth();

        return chartDrawingData;
    }

    /**
     * Compute units for the x-axis and keep it in the drawingData object
     */
    private void createDrawingData_X(final GraphDrawingData drawingData) {

        final ChartDataXSerie xData = drawingData.getXData();

        final double graphMaxValue = xData.getOriginalMaxValue();
        final long devVirtualGraphWidth = componentGraph.getXXDevGraphWidth();

        drawingData.devVirtualGraphWidth = devVirtualGraphWidth;

        //      final double scaleX = ((double) devVirtualGraphWidth - 1) / graphMaxValue;
        final double scaleX = (devVirtualGraphWidth) / graphMaxValue;
        drawingData.setScaleX(scaleX);

        /*
         * calculate the number of units which will be visible by dividing the visible length by the
         * minimum size which one unit should have in pixels
         */
        final long defaultUnitCount = devVirtualGraphWidth / _chart.gridHorizontalDistance;

        // unit raw value (not yet rounded) is the number in data values for one unit
        final double graphDefaultUnit = graphMaxValue / Math.max(1, defaultUnitCount);

        final int unitType = xData.getAxisUnit();
        switch (unitType) {

        case ChartDataSerie.X_AXIS_UNIT_DAY:

            createDrawingData_X_Day(drawingData);
            break;

        case ChartDataSerie.X_AXIS_UNIT_WEEK:

            createDrawingData_X_Week(drawingData);
            break;

        case ChartDataSerie.X_AXIS_UNIT_MONTH:

            createDrawingData_X_Month(drawingData);
            break;

        case ChartDataSerie.X_AXIS_UNIT_YEAR:

            createDrawingData_X_Year(drawingData);
            break;

        case ChartDataSerie.X_AXIS_UNIT_HISTORY:

            createDrawingData_X_History(drawingData, graphDefaultUnit);
            break;

        default:

            // get the unit list from the configuration
            final ArrayList<ChartUnit> xUnits = drawingData.getXUnits();

            // axis unit
            double graphUnit = 1; // this default value should be overwritten
            double majorValue = 0;

            switch (unitType) {
            case ChartDataSerie.AXIS_UNIT_HOUR_MINUTE_SECOND:
            case ChartDataSerie.AXIS_UNIT_HOUR_MINUTE_OPTIONAL_SECOND:
            case ChartDataSerie.AXIS_UNIT_HOUR_MINUTE:
                graphUnit = Util.roundTimeValue((long) graphDefaultUnit, false);
                majorValue = Util.getMajorTimeValue((long) graphUnit, false);
                break;

            case ChartDataSerie.AXIS_UNIT_NUMBER:
            case ChartDataSerie.X_AXIS_UNIT_NUMBER_CENTER:
                // unit is a decimal number
                graphUnit = Util.roundDecimalValue(graphDefaultUnit);
                majorValue = Util.getMajorDecimalValue(graphUnit);
                break;

            default:
                break;
            }

            /*
             * create units for the x-axis
             */

            // get the unitOffset when a startValue is set
            final double xStartValue = xData.getStartValue();
            double unitOffset = 0;
            if (xStartValue != 0) {
                unitOffset = xStartValue % graphUnit;
            }

            final int valueDivisor = xData.getValueDivisor();

            /**
             * This implementation do NOT support extended scaling (logarithmic scaling)
             */

            /*
             * increase by one unit that the right side of the chart is drawing a unit, in some
             * cases this didn't occured
             */
            double graphMinVisibleValue = xData.getVisibleMinValue();
            double graphMaxVisibleValue = xData.getVisibleMaxValue() + graphUnit;

            // decrease min value when it does not fit to unit borders
            final double graphMinRemainder = graphMinVisibleValue % graphUnit;
            graphMinVisibleValue = graphMinVisibleValue - graphMinRemainder;

            graphMinVisibleValue = Util.roundValueToUnit(graphMinVisibleValue, graphUnit, true);
            graphMaxVisibleValue = Util.roundValueToUnit(graphMaxVisibleValue, graphUnit, false);

            int loopCounter = 0;
            double graphValue = graphMinVisibleValue;

            while (graphValue <= graphMaxVisibleValue) {

                // create unit value/label
                final double unitPos = graphValue - unitOffset;
                double unitLabelValue = unitPos + xStartValue;

                if ((unitType == ChartDataSerie.AXIS_UNIT_HOUR_MINUTE_SECOND //
                        || unitType == ChartDataSerie.AXIS_UNIT_HOUR_MINUTE_OPTIONAL_SECOND) && xStartValue > 0) {

                    /*
                     * x-axis shows day time, start with 0:00 at midnight
                     */

                    unitLabelValue = unitLabelValue % DAY_IN_SECONDS;
                }

                final String unitLabel = net.tourbook.chart.Util.formatValue((float) unitLabelValue, unitType,
                        valueDivisor, true);

                final boolean isMajorValue = unitLabelValue % majorValue == 0;

                xUnits.add(new ChartUnit(unitPos, unitLabel, isMajorValue));

                // check for an infinity loop
                if (graphValue == graphMaxValue || loopCounter++ > 10000) {
                    break;
                }

                graphValue += graphUnit;
                graphValue = Util.roundValueToUnit(graphValue, graphUnit, false);
            }

            break;
        }

        /*
         * configure bars in the bar charts
         */
        if (_chartDataModel.getChartType() == ChartType.BAR) {

            final double[] highValues = xData.getHighValuesDouble()[0];

            if (unitType == ChartDataSerie.AXIS_UNIT_NUMBER || unitType == ChartDataSerie.AXIS_UNIT_HOUR_MINUTE) {

                final int barWidth = (int) ((devVirtualGraphWidth / highValues.length) / 2);

                drawingData.setBarRectangleWidth(Math.max(0, barWidth));
                drawingData.setBarPosition(GraphDrawingData.BAR_POS_CENTER);

            } else if (unitType == ChartDataSerie.X_AXIS_UNIT_NUMBER_CENTER) {

                /*
                 * set bar width that it is wide enouth to overlap the next right bar, the
                 * overlapped part will be removed in ChartComponentGraph.draw210BarGraph()
                 */
                final float barWidth = ((float) devVirtualGraphWidth / (highValues.length - 1));
                final int barWidth2 = (int) (Math.max(1, barWidth) * 1.10);

                drawingData.setBarRectangleWidth(barWidth2);
                drawingData.setBarPosition(GraphDrawingData.BAR_POS_CENTER);
            }
        }
    }

    private void createDrawingData_X_Day(final GraphDrawingData drawingData) {

        final ChartSegments chartSegments = drawingData.getXData().getChartSegments();
        final long devVirtualGraphWidth = componentGraph.getXXDevGraphWidth();

        createMonthUnequalUnits(drawingData, devVirtualGraphWidth, chartSegments.years, chartSegments.yearDays);

        // compute the width of the rectangles
        final int allDaysInAllYears = chartSegments.allValues;
        drawingData.setBarRectangleWidth((int) Math.max(0, (devVirtualGraphWidth / allDaysInAllYears)));
        drawingData.setXUnitTextPos(GraphDrawingData.X_UNIT_TEXT_POS_CENTER);

        drawingData.setScaleX((double) devVirtualGraphWidth / allDaysInAllYears);
    }

    private void createDrawingData_X_History(final GraphDrawingData graphDrawingData,
            final double graphDefaultUnitD) {

        final ChartDataXSerie xData = graphDrawingData.getXData();
        final double scaleX = graphDrawingData.getScaleX();

        final long graphMaxValue = (long) xData.getOriginalMaxValue();
        final long graphDefaultUnit = (long) graphDefaultUnitD;

        // get start time without mills
        final DateTime tourStartTime = xData.getStartDateTime().minus(xData.getStartDateTime().getMillisOfSecond());
        final DateTime tourEndTime = tourStartTime.plus(graphMaxValue * 1000);

        long unitStart = tourStartTime.getMillis();
        long unitEnd = graphMaxValue;
        long firstUnitYear = tourStartTime.getYear();
        long lastUnitYear = tourEndTime.getYear();

        long roundedYearUnit = 0;
        long majorRoundedYearUnit = 0;
        final double dev1Year = scaleX * YEAR_IN_SECONDS;
        final double dev1Month = scaleX * MONTH_IN_SECONDS;

        //      System.out.println(UI.timeStampNano() + " \t");
        //      System.out.println(UI.timeStampNano() + " createDrawingData_X_History\t" + " start: " + tourStartTime);
        //      // TODO remove SYSTEM.OUT.PRINTLN

        final double devTitleVisibleUnit = _devAllMonthLabelWidth * 1.2;

        final boolean isYearRounded = dev1Year < _devYearLabelWidth * 4;
        if (isYearRounded) {

            /*
             * adjust years to the rounded values
             */

            final double unitYears = (double) graphDefaultUnit / YEAR_IN_SECONDS;

            roundedYearUnit = Util.roundSimpleNumberUnits((long) unitYears);
            majorRoundedYearUnit = Util.getMajorSimpleNumberValue(roundedYearUnit);

            final long firstHistoryYear = tourStartTime.getYear();

            // decrease min value when it does not fit to unit borders
            final long yearMinRemainder = firstHistoryYear % roundedYearUnit;
            final long yearMinValue = firstHistoryYear - yearMinRemainder;

            final long yearMaxValue = lastUnitYear - (lastUnitYear % roundedYearUnit) + roundedYearUnit;

            unitStart = new DateTime((int) yearMinValue, 1, 1, 0, 0, 0, 0).getMillis();
            unitEnd = new DateTime((int) yearMaxValue, 12, 31, 23, 59, 59, 999).getMillis();
            firstUnitYear = yearMinValue;
            lastUnitYear = yearMaxValue;
        }

        /*
         * check if history units must be created, this is done only once for a tour to optimize it
         */
        if (unitStart != _historyUnitStart || unitEnd != _historyUnitDuration) {

            _historyUnitStart = unitStart;
            _historyUnitDuration = unitEnd;

            createHistoryUnits((int) firstUnitYear, (int) lastUnitYear);
        }

        graphDrawingData.setXUnitTextPos(GraphDrawingData.X_UNIT_TEXT_POS_CENTER);
        graphDrawingData.setIsXUnitOverlapChecked(true);
        graphDrawingData.setIsCheckUnitBorderOverlap(false);

        final HistoryTitle historyTitle = new HistoryTitle();
        xData.setHistoryTitle(historyTitle);

        // hide default unit
        xData.setUnitLabel(UI.EMPTY_STRING);

        final double devGraphXOffset = componentGraph.getXXDevViewPortLeftBorder();
        final int devVisibleWidth = getDevVisibleChartWidth();

        final long graphLeftBorder = (long) (devGraphXOffset / scaleX);
        final long graphRightBorder = (long) ((devGraphXOffset + devVisibleWidth) / scaleX);

        final ArrayList<ChartUnit> xUnits = graphDrawingData.getXUnits();
        final ArrayList<ChartUnit> xUnitTitles = new ArrayList<ChartUnit>();

        final ArrayList<Long> titleValueStart = historyTitle.graphStart = new ArrayList<Long>();
        final ArrayList<Long> titleValueEnd = historyTitle.graphEnd = new ArrayList<Long>();
        final ArrayList<String> titleText = historyTitle.titleText = new ArrayList<String>();

        final boolean isTimeSerieWithTimeZoneAdjustment = xData.isTimeSerieWithTimeZoneAdjustment();

        //      DateTime graphTime = tourStartTime.plus(graphLeftBorder * 1000);
        //      if (isTimeSerieWithTimeZoneAdjustment) {
        //         if (graphTime.getMillis() > UI.beforeCET) {
        //            graphTime = graphTime.minus(UI.BERLIN_HISTORY_ADJUSTMENT * 1000);
        //         }
        //      }
        //
        ////      final int graphSecondsOfDay = graphTime.getSecondOfDay();
        ////      final DateTime graphNextDay = graphTime.plus((DAY_IN_SECONDS - graphSecondsOfDay) * 1000);
        //
        //      System.out.println(UI.timeStampNano());
        //      System.out.println(UI.timeStampNano() + " tourStartTime " + tourStartTime);
        //      System.out.println(UI.timeStampNano() + " graphTime     " + graphTime);
        ////      System.out.println(UI.timeStampNano() + " graphNextDay  " + graphNextDay);
        //      System.out.println(UI.timeStampNano());
        //      // TODO remove SYSTEM.OUT.PRINTLN

        if (isYearRounded) {

            /*
             * create units for rounded years
             */

            //         System.out.println(UI.timeStampNano() + "\trounded years\t");
            //         // TODO remove SYSTEM.OUT.PRINTLN

            graphDrawingData.setXUnitTextPos(GraphDrawingData.X_UNIT_TEXT_POS_LEFT);

            int historyYearIndex = 0;

            /*
             * start unit at the first day of the first year at 0:00:00, this is necessary that the
             * unit is positioned exactly
             */
            final int startDOY = tourStartTime.getDayOfYear();
            final int startDaySeconds = tourStartTime.secondOfDay().get();

            final int startYear = tourStartTime.getYear();

            int yearIndex = 0;
            long graphYearOffset = 0;
            while (startYear > _historyYears[yearIndex]) {
                graphYearOffset += _historyDOY[yearIndex++] * DAY_IN_SECONDS;
            }

            long graphValue = -startDOY * DAY_IN_SECONDS - startDaySeconds - graphYearOffset;

            // loop: years
            while (graphValue <= graphMaxValue) {

                long graphUnit = 0;

                for (int unitIndex = 0; unitIndex < roundedYearUnit; unitIndex++) {

                    final int unitYearIndex = historyYearIndex + unitIndex;

                    // graph unit = rounded years
                    graphUnit += _historyDOY[unitYearIndex] * DAY_IN_SECONDS;
                }

                if (graphValue < graphLeftBorder - graphUnit //
                //
                // ensure it's 366 days
                        - DAY_IN_SECONDS) {

                    // advance to the next unit
                    graphValue += graphUnit;
                    historyYearIndex += roundedYearUnit;

                    continue;
                }

                if (graphValue > graphRightBorder) {
                    break;
                }

                /*
                 * draw year tick
                 */
                final int yearValue = _historyYears[historyYearIndex];

                final boolean isMajorValue = yearValue % majorRoundedYearUnit == 0;

                xUnits.add(new ChartUnit(graphValue + DAY_IN_SECONDS, UI.EMPTY_STRING, isMajorValue));

                /*
                 * draw title
                 */
                titleValueStart.add(graphValue);
                titleValueEnd.add(graphValue + graphUnit - 1);
                titleText.add(Integer.toString(yearValue));

                // advance to the next rounded unit
                graphValue += graphUnit;
                historyYearIndex += roundedYearUnit;
            }

        } else if (dev1Year < _devAllMonthLabelWidth * 12) {

            /*
             * create units for year/month
             */

            //         System.out.println(UI.timeStampNano() + "\tyear/month\t");
            //         // TODO remove SYSTEM.OUT.PRINTLN

            graphDrawingData.setTitleTextPos(GraphDrawingData.X_UNIT_TEXT_POS_CENTER);
            graphDrawingData.setXUnitTextPos(GraphDrawingData.X_UNIT_TEXT_POS_CENTER);

            int historyYearIndex = 0;

            // start unit at the first day of the first year at 0:00:00
            final int startDOY = tourStartTime.getDayOfYear();
            final int startSeconds = tourStartTime.secondOfDay().get();
            long graphValue = -startDOY * DAY_IN_SECONDS - startSeconds;

            // loop: years
            while (graphValue <= graphMaxValue) {

                // graph unit = 1 year
                final long graphUnit = _historyDOY[historyYearIndex] * DAY_IN_SECONDS;

                if (graphValue < graphLeftBorder - graphUnit //
                //
                // ensure it's 366 days
                        - DAY_IN_SECONDS) {

                    // advance to the next unit
                    graphValue += graphUnit;
                    historyYearIndex++;

                    continue;
                }

                if (graphValue > graphRightBorder) {
                    break;
                }

                final int devUnitWidth = (int) (scaleX * graphUnit);
                final int[] historyMonthDays = _historyMonths[historyYearIndex];

                /*
                 * draw year tick
                 */
                xUnits.add(new ChartUnit(graphValue + DAY_IN_SECONDS, UI.EMPTY_STRING, true));

                /*
                 * draw year title
                 */
                {
                    final String yearLabel = Integer.toString(_historyYears[historyYearIndex]);

                    /*
                     * get number of repeated year labels within a year unit
                     */
                    int repeatedMonths = 1;

                    while (true) {
                        if (devUnitWidth / repeatedMonths < devTitleVisibleUnit) {
                            break;
                        }
                        repeatedMonths++;
                    }

                    // ensure array size is big enough (*2)
                    final int[] monthStarts = new int[repeatedMonths * 2];
                    final int[] monthEnds = new int[repeatedMonths * 2];
                    final int monthRepeats = 12 / repeatedMonths;

                    int yearMonthDOY = 0;
                    int repeatIndex = 0;

                    for (int monthIndex = 0; monthIndex < 12; monthIndex++) {

                        final int monthDays = historyMonthDays[monthIndex];

                        if (monthIndex % monthRepeats == 0) {

                            if (repeatIndex > 0) {
                                monthEnds[repeatIndex - 1] = yearMonthDOY;
                            }

                            monthStarts[repeatIndex] = yearMonthDOY;

                            repeatIndex++;
                        }

                        yearMonthDOY += monthDays;
                    }
                    monthEnds[repeatIndex - 1] = yearMonthDOY;

                    for (int repeatIndex2 = 0; repeatIndex2 < monthStarts.length; repeatIndex2++) {

                        final int monthStart = monthStarts[repeatIndex2];
                        final int monthEnd = monthEnds[repeatIndex2];

                        // skip invalid entries
                        if (monthStart == 0 && monthEnd == 0) {
                            break;
                        }

                        titleValueStart.add(graphValue + monthStart * DAY_IN_SECONDS + DAY_IN_SECONDS);
                        titleValueEnd.add(graphValue + monthEnd * DAY_IN_SECONDS + DAY_IN_SECONDS);

                        titleText.add(yearLabel);
                    }
                }

                /*
                 * draw x-axis units
                 */

                if (devUnitWidth >= _devAllMonthLabelWidth * 1.2) {

                    createHistoryMonthUnits_Months(xUnits, historyMonthDays, graphValue, 1, 12, true);

                } else if (devUnitWidth >= _devAllMonthLabelWidth * 1) {

                    createHistoryMonthUnits_Months(xUnits, historyMonthDays, graphValue, 3, 0, false);

                } else if (devUnitWidth >= _devAllMonthLabelWidth * 0.7) {

                    createHistoryMonthUnits_Months(xUnits, historyMonthDays, graphValue, 6, 0, false);
                }

                // advance to the next unit
                graphValue += graphUnit;
                historyYearIndex++;
            }

        } else if (dev1Month < _devAllMonthLabelWidth * 30) {

            /*
             * create units for month/day
             */

            //         System.out.println(UI.timeStampNano() + "\tmonth/day");
            //         // TODO remove SYSTEM.OUT.PRINTLN

            graphDrawingData.setTitleTextPos(GraphDrawingData.X_UNIT_TEXT_POS_CENTER);

            int historyYearIndex = 0;

            // start unit at the first day of the first year at 0:00:00
            final int startDOY = tourStartTime.getDayOfYear();
            final int startSeconds = tourStartTime.secondOfDay().get();
            long graphValue = -startDOY * DAY_IN_SECONDS - startSeconds;

            monthLoop:

            // loop: months
            while (graphValue <= graphMaxValue) {

                final int[] yearMonths = _historyMonths[historyYearIndex];

                for (int monthIndex = 0; monthIndex < yearMonths.length; monthIndex++) {

                    final int monthDays = yearMonths[monthIndex];

                    // graph unit = 1 month
                    final long graphUnit = monthDays * DAY_IN_SECONDS;

                    if (graphValue < graphLeftBorder - graphUnit) {

                        // advance to the next month unit
                        graphValue += graphUnit;

                        continue;
                    }

                    if (graphValue > graphRightBorder) {
                        break monthLoop;
                    }

                    /*
                     * draw month tick
                     */
                    xUnits.add(new ChartUnit(graphValue + DAY_IN_SECONDS, UI.EMPTY_STRING, true));

                    /*
                     * create title units
                     */
                    {
                        final String monthTitle = _monthLabels[monthIndex] + UI.SPACE2
                                + Integer.toString(_historyYears[historyYearIndex]);

                        // get number of repeated labels within one graph unit
                        int repeatedDays = 1;
                        final int devUnitWidth = (int) (scaleX * graphUnit);

                        while (true) {
                            if (devUnitWidth / repeatedDays < devTitleVisibleUnit) {
                                break;
                            }
                            repeatedDays++;
                        }

                        // ensure array size is big enough (*2)
                        final int[] dayStarts = new int[repeatedDays * 2];
                        final int[] dayEnds = new int[repeatedDays * 2];
                        final int repeatedDayUnit = monthDays / repeatedDays;

                        int dayStartEnd = 0;
                        int repeatIndex = 0;

                        for (int dayIndex = 0; dayIndex < monthDays; dayIndex++) {

                            if (dayIndex % repeatedDayUnit == 0) {

                                if (repeatIndex > 0) {
                                    dayEnds[repeatIndex - 1] = dayStartEnd;
                                }

                                dayStarts[repeatIndex] = dayStartEnd;

                                repeatIndex++;
                            }

                            dayStartEnd += 1;
                        }
                        dayEnds[repeatIndex - 1] = dayStartEnd;

                        for (int repeatIndex2 = 0; repeatIndex2 < dayStarts.length; repeatIndex2++) {

                            final int dayStart = dayStarts[repeatIndex2];
                            final int dayEnd = dayEnds[repeatIndex2];

                            // skip invalid entries
                            if (dayStart == 0 && dayEnd == 0) {
                                break;
                            }

                            titleValueStart.add(graphValue + dayStart * DAY_IN_SECONDS + DAY_IN_SECONDS);
                            titleValueEnd.add(graphValue + dayEnd * DAY_IN_SECONDS + DAY_IN_SECONDS);
                            titleText.add(monthTitle);
                        }
                    }

                    /*
                     * draw x-axis units: day number in month
                     */
                    final double unitDays = (double) graphDefaultUnit / DAY_IN_SECONDS;

                    final int roundedDayUnit = Util.roundSimpleNumberUnits((long) unitDays);

                    if (roundedDayUnit == 1) {
                        graphDrawingData.setXUnitTextPos(GraphDrawingData.X_UNIT_TEXT_POS_CENTER);
                    } else {
                        graphDrawingData.setXUnitTextPos(GraphDrawingData.X_UNIT_TEXT_POS_LEFT);
                    }

                    int dayNo = roundedDayUnit;
                    while (dayNo <= monthDays) {

                        xUnits.add(new ChartUnit(//
                                graphValue + (dayNo * DAY_IN_SECONDS), Integer.toString(dayNo), false));

                        dayNo += roundedDayUnit;
                    }

                    // advance to the next month unit
                    graphValue += graphUnit;
                }

                historyYearIndex++;
            }

        } else {

            /*
             * create units for day/seconds
             */

            //         System.out.println(UI.timeStampNano() + " day/seconds");
            //         // TODO remove SYSTEM.OUT.PRINTLN

            graphDrawingData.setTitleTextPos(GraphDrawingData.X_UNIT_TEXT_POS_CENTER);
            graphDrawingData.setXUnitTextPos(GraphDrawingData.X_UNIT_TEXT_POS_LEFT);

            final long graphUnit = Util.roundTime24h(graphDefaultUnit);
            final long majorUnit = Util.getMajorTimeValue24(graphUnit);

            final int startSeconds = tourStartTime.secondOfDay().get();
            final long startUnitOffset = startSeconds % graphUnit;

            // decrease min value when it does not fit to unit borders, !!! VERY IMPORTANT !!!
            final long graphValueStart = graphLeftBorder - graphLeftBorder % graphUnit;

            final long graphMaxVisibleValue = graphRightBorder + graphUnit;
            long graphValue = graphValueStart;

            /*
             * create x-axis units
             */
            while (graphValue <= graphMaxVisibleValue) {

                // create unit value/label
                final long unitValueAdjusted = graphValue - startUnitOffset;
                final long unitValueStart = unitValueAdjusted + startSeconds;

                final long unitValue = unitValueStart % DAY_IN_SECONDS;

                final String unitLabel = net.tourbook.chart.Util.format_hh_mm_ss_Optional(unitValue);

                final boolean isMajorValue = unitValue % majorUnit == 0;

                xUnits.add(new ChartUnit(unitValueAdjusted, unitLabel, isMajorValue));

                graphValue += graphUnit;
            }

            /*
             * create dummy units before and after the real units that the title is displayed also
             * at the border, title is displayed between major units
             */
            final int numberOfSmallUnits = (int) (majorUnit / graphUnit);
            long titleUnitStart = (long) xUnits.get(0).value;

            for (int unitIndex = numberOfSmallUnits; unitIndex > 0; unitIndex--) {

                final long unitValueAdjusted = titleUnitStart - (graphUnit * unitIndex);

                final long unitValue = (unitValueAdjusted + startSeconds) % DAY_IN_SECONDS;
                final boolean isMajorValue = unitValue % majorUnit == 0;

                final String unitLabel = net.tourbook.chart.Util.format_hh_mm_ss_Optional(unitValue);

                xUnitTitles.add(new ChartUnit(unitValueAdjusted, unitLabel, isMajorValue));
            }

            xUnitTitles.addAll(xUnits);

            titleUnitStart = (long) xUnitTitles.get(xUnitTitles.size() - 1).value;

            for (int unitIndex = 1; unitIndex < numberOfSmallUnits * 1; unitIndex++) {

                final long unitValueAdjusted = titleUnitStart + (graphUnit * unitIndex);

                final long unitValue = (unitValueAdjusted + startSeconds) % DAY_IN_SECONDS;
                final boolean isMajorValue = unitValue % majorUnit == 0;

                final String unitLabel = net.tourbook.chart.Util.format_hh_mm_ss_Optional(unitValue);

                xUnitTitles.add(new ChartUnit(unitValueAdjusted, unitLabel, isMajorValue));
            }

            /*
             * create title units
             */
            long prevGraphUnitValue = Long.MIN_VALUE;

            for (int unitIndex = 0; unitIndex < xUnitTitles.size(); unitIndex++) {

                final ChartUnit chartUnit = xUnitTitles.get(unitIndex);
                if (chartUnit.isMajorValue) {

                    final long currentGraphUnitValue = (long) chartUnit.value;

                    if (prevGraphUnitValue != Long.MIN_VALUE) {

                        titleValueStart.add(prevGraphUnitValue);
                        titleValueEnd.add(currentGraphUnitValue - 1);

                        long graphDay = tourStartTime.getMillis() + prevGraphUnitValue * 1000;

                        if (isTimeSerieWithTimeZoneAdjustment) {

                            if (graphDay > UI.beforeCET) {
                                graphDay -= UI.BERLIN_HISTORY_ADJUSTMENT * 1000;
                            }
                        }

                        final String dayTitle = _dtFormatter.print(graphDay);

                        titleText.add(dayTitle);
                    }

                    prevGraphUnitValue = currentGraphUnitValue;
                }
            }
        }

        //      System.out.println(UI.timeStampNano() + " \t");
        //
        //      for (final ChartUnit xUnit : xUnits) {
        //         System.out.println(UI.timeStampNano() + " \t" + xUnit);
        //         // TODO remove SYSTEM.OUT.PRINTLN
        //      }
        //
        //
        //      for (final ChartUnit xUnit : xUnitTitles) {
        //         System.out.println(UI.timeStampNano() + " \t" + xUnit);
        //         // TODO remove SYSTEM.OUT.PRINTLN
        //      }
        //
        //      for (int unitIndex = 0; unitIndex < titleText.size(); unitIndex++) {
        //
        //         System.out.println(UI.timeStampNano()
        //               + ("\t" + titleText.get(unitIndex))
        //               + ("\t" + (long) ((long) (titleValueStart.get(unitIndex) * scaleX) - devGraphXOffset))
        //               + ("\t" + (long) ((long) (titleValueEnd.get(unitIndex) * scaleX) - devGraphXOffset))
        //               + ("\t" + titleValueStart.get(unitIndex))
        //               + ("\t" + titleValueEnd.get(unitIndex))
        //         //
        //               );
        //         // TODO remove SYSTEM.OUT.PRINTLN
        //      }
    }

    private void createDrawingData_X_Month(final GraphDrawingData drawingData) {

        final ChartDataXSerie xData = drawingData.getXData();

        final int allUnitsSize = xData._highValuesDouble[0].length;
        final long devVirtualGraphWidth = componentGraph.getXXDevGraphWidth();
        final double scaleX = (double) devVirtualGraphWidth / allUnitsSize;
        drawingData.setScaleX(scaleX);

        final int numberOfYears = xData.getChartSegments().segmentTitle.length;

        createMonthEqualUnits(drawingData, devVirtualGraphWidth, allUnitsSize, numberOfYears);

        // compute the width and position of the rectangles
        final float monthWidth = (float) Math.max(0, (scaleX) - 1);
        final float barWidth = Math.max(0, (monthWidth * 0.90f));

        drawingData.setBarRectangleWidth((int) barWidth);
        drawingData.setDevBarRectangleXPos((int) (Math.max(0, (monthWidth - barWidth) / 2) + 1));

        drawingData.setXUnitTextPos(GraphDrawingData.X_UNIT_TEXT_POS_CENTER);
    }

    private void createDrawingData_X_Week(final GraphDrawingData drawingData) {

        final ChartDataXSerie xData = drawingData.getXData();
        final long devVirtualGraphWidth = componentGraph.getXXDevGraphWidth();

        final double[] xValues = xData.getHighValuesDouble()[0];
        final int allWeeks = xValues.length;
        final float devWeekWidth = devVirtualGraphWidth / allWeeks;

        final ChartSegments chartSegments = drawingData.getXData().getChartSegments();

        final int[] yearDays = chartSegments.yearDays;

        int allDaysInAllYears = 0;
        for (final int days : yearDays) {
            allDaysInAllYears += days;
        }

        createMonthUnequalUnits(drawingData, devVirtualGraphWidth, chartSegments.years, yearDays);

        final float barWidth = devWeekWidth * 0.7f;

        drawingData.setBarRectangleWidth((int) barWidth);
        drawingData.setDevBarRectangleXPos((int) (Math.max(0, (devWeekWidth - 2) / 2) + 1));

        drawingData.setBarPosition(GraphDrawingData.BAR_POS_CENTER);
        drawingData.setXUnitTextPos(GraphDrawingData.X_UNIT_TEXT_POS_CENTER);

        drawingData.setScaleX((double) devVirtualGraphWidth / allWeeks);
        drawingData.setScaleUnitX((double) devVirtualGraphWidth / allDaysInAllYears);

    }

    private void createDrawingData_X_Year(final GraphDrawingData drawingData) {

        final ChartDataYSerie yData = drawingData.getYData();
        final ChartDataXSerie xData = drawingData.getXData();

        final ChartSegments chartSegments = drawingData.getXData().getChartSegments();
        final int[] yearValues = chartSegments.years;
        final long devVirtualGraphWidth = componentGraph.getXXDevGraphWidth();

        final int yearCounter = xData._highValuesDouble[0].length;
        final double scaleX = (double) devVirtualGraphWidth / yearCounter;
        drawingData.setScaleX(scaleX);

        // create year units
        final ArrayList<ChartUnit> xUnits = drawingData.getXUnits();
        for (int yearIndex = 0; yearIndex < yearValues.length; yearIndex++) {
            xUnits.add(new ChartUnit(yearIndex, Integer.toString(yearValues[yearIndex])));
        }

        // compute the width and position of the rectangles
        int barWidth;
        final int yearWidth = (int) Math.max(0, scaleX - 1);

        switch (yData.getChartLayout()) {
        case ChartDataYSerie.BAR_LAYOUT_SINGLE_SERIE:
        case ChartDataYSerie.BAR_LAYOUT_STACKED:

            // the bar's width is 50% of the width for a month
            barWidth = (int) Math.max(0, (yearWidth * 0.9f));
            drawingData.setBarRectangleWidth(barWidth);
            drawingData.setDevBarRectangleXPos((Math.max(0, (yearWidth - barWidth) / 2) + 1));
            break;

        case ChartDataYSerie.BAR_LAYOUT_BESIDE:

            final int serieCount = yData.getHighValuesFloat().length;

            // the bar's width is 75% of the width for a month
            barWidth = (int) Math.max(0, yearWidth * 0.9f);
            //         if (serieCount == 1) {
            //
            //            drawingData.setBarRectangleWidth(Math.max(1, barWidth));
            ////         drawingData.setDevBarRectangleXPos((int) (Math.max(0, (yearWidth - barWidth) / 2) + 2));
            ////         drawingData.setDevBarRectangleXPos(0);
            //
            //         } else {
            final int singleBarWidth = Math.max(1, barWidth / (serieCount - 0));
            drawingData.setBarRectangleWidth(singleBarWidth);
            final int barPosition = (yearWidth - (singleBarWidth * (serieCount - 0))) / 2;

            drawingData.setDevBarRectangleXPos((Math.max(0, barPosition) + 0));
            //            drawingData.setDevBarRectangleXPos(0);
            //         }

        default:
            break;
        }

        drawingData.setXUnitTextPos(GraphDrawingData.X_UNIT_TEXT_POS_CENTER);
    }

    /**
     * computes data for the y axis
     * 
     * @param drawingData
     * @param graphCount
     * @param currentGraph
     */
    private void createDrawingData_Y(final GraphDrawingData drawingData, final int graphCount,
            final int currentGraph) {

        final int unitType = drawingData.getYData().getAxisUnit();

        if (unitType == ChartDataSerie.AXIS_UNIT_NUMBER) {

            createDrawingData_Y_Numbers(drawingData, graphCount, currentGraph);

        } else if (unitType == ChartDataSerie.AXIS_UNIT_HOUR_MINUTE
                || unitType == ChartDataSerie.AXIS_UNIT_HOUR_MINUTE_24H
                || unitType == ChartDataSerie.AXIS_UNIT_HOUR_MINUTE_SECOND
                || unitType == ChartDataSerie.AXIS_UNIT_MINUTE_SECOND) {

            createDrawingData_Y_Time(drawingData, graphCount, currentGraph);

        } else if (unitType == ChartDataSerie.AXIS_UNIT_HISTORY) {

            createDrawingData_Y_History(drawingData, graphCount, currentGraph);
        }
    }

    private void createDrawingData_Y_History(final GraphDrawingData drawingData, final int graphCount,
            final int currentGraph) {

        // height of one chart graph including the slider bar
        final int devChartHeight = getDevChartHeightWithoutTrim();

        int devGraphHeight = devChartHeight;

        /*
         * adjust graph device height for stacked graphs, a gap is between two graphs
         */
        if (_chartDataModel.isStackedChart() && graphCount > 1) {
            final int devGraphHeightSpace = devGraphHeight - (_chartsVerticalDistance * (graphCount - 1));
            devGraphHeight = (devGraphHeightSpace / graphCount);
        }

        // enforce minimum chart height
        devGraphHeight = Math.max(devGraphHeight, CHART_MIN_HEIGHT);

        // remove slider bar from graph height
        devGraphHeight -= _devSliderBarHeight;

        // calculate the vertical device offset
        int devYTop = _devMarginTop + _devXTitleBarHeight;

        if (_chartDataModel.isStackedChart()) {
            // each chart has its own drawing rectangle which are stacked on
            // top of each other
            devYTop += (currentGraph * (devGraphHeight + _devSliderBarHeight))
                    + ((currentGraph - 1) * _chartsVerticalDistance);

        } else {
            // all charts are drawn on the same rectangle
            devYTop += devGraphHeight;
        }

        drawingData.setDevYBottom(devYTop);
        drawingData.setDevYTop(devYTop - devGraphHeight);

        drawingData.devGraphHeight = devGraphHeight;
        drawingData.setDevSliderHeight(0);
    }

    private void createDrawingData_Y_Numbers(final GraphDrawingData drawingData, final int graphCount,
            final int currentGraph) {

        final ChartDataYSerie yData = drawingData.getYData();
        final int unitType = yData.getAxisUnit();

        // height of one chart graph including the slider bar
        final int devChartHeight = getDevChartHeightWithoutTrim();

        int devGraphHeight = devChartHeight;

        /*
         * adjust graph device height for stacked graphs, a gap is between two graphs
         */
        if (_chartDataModel.isStackedChart() && graphCount > 1) {
            final int devGraphHeightSpace = devGraphHeight - (_chartsVerticalDistance * (graphCount - 1));
            devGraphHeight = (devGraphHeightSpace / graphCount);
        }

        // enforce minimum chart height
        devGraphHeight = Math.max(devGraphHeight, CHART_MIN_HEIGHT);

        // remove slider bar from graph height
        devGraphHeight -= _devSliderBarHeight;

        /*
         * all variables starting with graph... contain data values from the graph which are not
         * scaled to the device
         */

        final double graphMinValue = yData.getVisibleMinValue();
        final double graphMaxValue = yData.getVisibleMaxValue();

        final double defaultValueRange = graphMaxValue > 0 ? (graphMaxValue - graphMinValue)
                : -(graphMinValue - graphMaxValue);

        /*
         * calculate the number of units which will be visible by dividing the available height by
         * the minimum size which one unit should have in pixels
         */
        final int defaultUnitCount = devGraphHeight / _chart.gridVerticalDistance;

        // defaultUnitValue is the number in data values for one unit
        final double defaultUnitValue = defaultValueRange / Math.max(1, defaultUnitCount);

        // round the unit
        final double graphUnit = Util.roundDecimalValue(defaultUnitValue);

        /*
         * !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
         * The scaled unit with long min/max values is used because arithmetic with floating point
         * values fails. BigDecimal is necessary otherwise the scaledUnit can be wrong !!!
         * !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
         */
        final long valueScaling = Util.getValueScaling(graphUnit);

        final BigDecimal bigGraphUnit = new BigDecimal(Double.valueOf(graphUnit));
        final BigDecimal bigValueScaling = new BigDecimal(valueScaling);
        final BigDecimal bigScaledUnit = bigGraphUnit.multiply(bigValueScaling);

        final long scaledUnit = bigScaledUnit.longValue();

        long scaledMinValue = (long) (graphMinValue * valueScaling);
        long scaledMaxValue = (long) (graphMaxValue * valueScaling);

        /*
         * adjustedYValue is set when the y-slider has been moved
         */
        boolean isMinAdjusted = false;
        boolean isMaxAdjusted = false;
        final float adjustedYValue = yData.adjustedYValue;
        final boolean isMinMaxAdjusted = adjustedYValue != Float.MIN_VALUE;

        if (isMinMaxAdjusted) {

            final long scaledAdjustedYValue = (long) (adjustedYValue * valueScaling);

            isMinAdjusted = scaledAdjustedYValue == scaledMinValue;
            isMaxAdjusted = scaledAdjustedYValue == scaledMaxValue;
        }

        /*
         * adjust min value, decrease min value when it does not fit to unit borders
         */
        float adjustMinValue = 0;
        final long minRemainder = scaledMinValue % scaledUnit;
        if (minRemainder != 0 && scaledMinValue < 0) {
            adjustMinValue = scaledUnit;
        }
        final long adjustedScaledMinValue = (long) ((scaledMinValue - adjustMinValue) / scaledUnit) * scaledUnit;

        /*
         * ensure that min value is not at the bottom of the graph, except values which start at 0
         */
        if (isMinMaxAdjusted == false && scaledMinValue == adjustedScaledMinValue && scaledMinValue != 0) {
            scaledMinValue = adjustedScaledMinValue - scaledUnit;
        } else if (isMinMaxAdjusted && isMinAdjusted) {
            scaledMinValue = adjustedScaledMinValue;// - scaledUnit;
        } else {
            scaledMinValue = adjustedScaledMinValue;
        }

        /*
         * adjust max value, increase the max value when it does not fit to unit borders
         */
        float adjustMaxValue = 0;
        final long maxRemainder = scaledMaxValue % scaledUnit;
        if (maxRemainder != 0) {
            adjustMaxValue = scaledUnit;
        }
        final long adjustedScaledMaxValue = ((long) ((scaledMaxValue + adjustMaxValue) / scaledUnit) * scaledUnit);

        // ensure that max value is not at the top of the graph
        if (isMinMaxAdjusted == false && scaledMaxValue == adjustedScaledMaxValue) {
            scaledMaxValue = adjustedScaledMaxValue + scaledUnit;
        } else if (isMinMaxAdjusted && isMaxAdjusted) {
            scaledMaxValue = adjustedScaledMaxValue;// + scaledUnit;
        } else {
            scaledMaxValue = adjustedScaledMaxValue;
        }

        /*
         * check that max is larger than min
         */
        if (scaledMinValue == scaledMaxValue) {

            scaledMinValue = scaledMinValue - scaledUnit;
            scaledMaxValue = scaledMaxValue + scaledUnit;

        } else if (scaledMinValue > scaledMaxValue) {
            /*
             * this case can happen when the min value is set in the pref dialog, this is more a
             * hack than a good solution
             */
            scaledMinValue = scaledMaxValue - (2 * scaledUnit);
        }

        final long scaledValueRange = scaledMaxValue > 0 ? (scaledMaxValue - scaledMinValue)
                : -(scaledMinValue - scaledMaxValue);

        // get major values according to the unit
        final double majorValue = Util.getMajorDecimalValue(graphUnit);

        // calculate the vertical scaling between graph and device
        final float value1 = (float) scaledValueRange / valueScaling;
        final float graphScaleY = devGraphHeight / value1;

        // calculate the vertical device offset
        int devYTop = _devMarginTop + _devXTitleBarHeight;

        if (_chartDataModel.isStackedChart()) {
            // each chart has its own drawing rectangle which are stacked on
            // top of each other
            devYTop += (currentGraph * (devGraphHeight + _devSliderBarHeight))
                    + ((currentGraph - 1) * _chartsVerticalDistance);

        } else {
            // all charts are drawn on the same rectangle
            devYTop += devGraphHeight;
        }

        drawingData.setScaleY(graphScaleY);

        drawingData.setDevYBottom(devYTop);
        drawingData.setDevYTop(devYTop - devGraphHeight);

        drawingData.setGraphYBottom((float) scaledMinValue / valueScaling);
        drawingData.setGraphYTop((float) scaledMaxValue / valueScaling);

        drawingData.devGraphHeight = devGraphHeight;
        drawingData.setDevSliderHeight(_devSliderBarHeight);

        final ArrayList<ChartUnit> unitList = drawingData.getYUnits();
        final int valueDivisor = yData.getValueDivisor();
        int loopCounter = 0;

        long scaledValue = scaledMinValue;

        // loop: create unit label for all units
        while (scaledValue <= scaledMaxValue) {

            final double descaledValue = (double) scaledValue / valueScaling;

            final String unitLabel = net.tourbook.chart.Util.formatValue((float) descaledValue, unitType,
                    valueDivisor, false);
            final boolean isMajorValue = descaledValue % majorValue == 0;

            unitList.add(new ChartUnit((float) descaledValue, unitLabel, isMajorValue));

            // prevent endless loops when the unit is 0
            if (scaledValue == scaledMaxValue || loopCounter++ > 1000) {
                break;
            }

            scaledValue += scaledUnit;
        }

        if (unitType == ChartDataSerie.AXIS_UNIT_HOUR_MINUTE_24H && scaledValue > scaledMaxValue) {
            unitList.add(new ChartUnit(scaledMaxValue / valueScaling, UI.EMPTY_STRING));
        }
    }

    private void createDrawingData_Y_Time(final GraphDrawingData drawingData, final int graphCount,
            final int currentGraph) {

        final ChartDataYSerie yData = drawingData.getYData();

        // height of one chart graph including the slider bar
        final int devChartHeight = getDevChartHeightWithoutTrim();

        int devGraphHeight = devChartHeight;

        // adjust graph device height for stacked graphs, a gap is between two
        // graphs
        if (_chartDataModel.isStackedChart() && graphCount > 1) {
            final int devGraphHeightSpace = (devGraphHeight - (_chartsVerticalDistance * (graphCount - 1)));
            devGraphHeight = (devGraphHeightSpace / graphCount);
        }

        // enforce minimum chart height
        devGraphHeight = Math.max(devGraphHeight, CHART_MIN_HEIGHT);

        // remove slider bar from graph height
        devGraphHeight -= _devSliderBarHeight;

        /*
         * all variables starting with graph... contain data values from the graph which are not
         * scaled to the device
         */

        final int unitType = yData.getAxisUnit();
        int graphMinValue = (int) yData.getVisibleMinValue();
        int graphMaxValue = (int) yData.getVisibleMaxValue();

        // clip max value
        boolean isAdjustGraphUnit = false;
        if (unitType == ChartDataSerie.AXIS_UNIT_HOUR_MINUTE_24H && (graphMaxValue / 3600 > 24)) {
            graphMaxValue = 24 * 3600;
            isAdjustGraphUnit = true;
        }

        int graphValueRange = graphMaxValue > 0 ? (graphMaxValue - graphMinValue)
                : -(graphMinValue - graphMaxValue);

        /*
         * calculate the number of units which will be visible by dividing the available height by
         * the minimum size which one unit should have in pixels
         */
        final float defaultUnitCount = devGraphHeight / _chart.gridVerticalDistance;

        // unitValue is the number in data values for one unit
        final float defaultUnitValue = graphValueRange / Math.max(1, defaultUnitCount);

        // round the unit
        long graphUnit = (long) Util.roundTimeValue(defaultUnitValue,
                unitType == ChartDataSerie.AXIS_UNIT_HOUR_MINUTE_24H);

        long adjustMinValue = 0;
        if ((graphMinValue % graphUnit) != 0 && graphMinValue < 0) {
            adjustMinValue = graphUnit;
        }
        graphMinValue = (int) ((int) ((graphMinValue - adjustMinValue) / graphUnit) * graphUnit);

        // adjust the min value so that bar graphs start at the bottom of the chart
        if (_chartDataModel.getChartType() == ChartType.BAR && _chart.getStartAtChartBottom()) {
            yData.setVisibleMinValue(graphMinValue);
        }

        // increase the max value when it does not fit to unit borders
        float adjustMaxValue = 0;
        if ((graphMaxValue % graphUnit) != 0) {
            adjustMaxValue = graphUnit;
        }
        graphMaxValue = (int) ((int) ((graphMaxValue + adjustMaxValue) / graphUnit) * graphUnit);

        if (isAdjustGraphUnit
                || unitType == ChartDataSerie.AXIS_UNIT_HOUR_MINUTE_24H && (graphMaxValue / 3600 > 24)) {

            // max value exeeds 24h

            // count number of units
            int unitCounter = 0;
            int graphValue = graphMinValue;
            while (graphValue <= graphMaxValue) {

                // prevent endless loops when the unit is 0
                if (graphValue == graphMaxValue) {
                    break;
                }
                unitCounter++;
                graphValue += graphUnit;
            }

            // adjust to 24h
            graphMaxValue = Math.min(24 * 3600, ((((int) yData.getVisibleMaxValue()) / 3600) * 3600) + 3600);

            // adjust to the whole hour
            graphMinValue = Math.max(0, ((((int) yData.getVisibleMinValue() / 3600) * 3600)));

            graphUnit = (graphMaxValue - graphMinValue) / unitCounter;
            graphUnit = (long) Util.roundTimeValue(graphUnit, unitType == ChartDataSerie.AXIS_UNIT_HOUR_MINUTE_24H);
        }

        graphValueRange = graphMaxValue > 0 ? //
                (graphMaxValue - graphMinValue) : -(graphMinValue - graphMaxValue);

        // ensure the chart is drawn correctly with pseudo data
        if (graphValueRange == 0) {
            graphValueRange = 3600;
            graphMaxValue = 3600;
            graphUnit = 1800;
        }

        final float majorValue = Util.getMajorTimeValue(//
                graphUnit, unitType == ChartDataSerie.AXIS_UNIT_HOUR_MINUTE_24H);

        // calculate the vertical scaling between graph and device
        final float graphScaleY = (float) (devGraphHeight) / graphValueRange;

        // calculate the vertical device offset
        int devYTop = _devMarginTop + _devXTitleBarHeight;

        if (_chartDataModel.isStackedChart()) {
            // each chart has its own drawing rectangle which are stacked on
            // top of each other
            devYTop += (currentGraph * (devGraphHeight + _devSliderBarHeight))
                    + ((currentGraph - 1) * _chartsVerticalDistance);

        } else {
            // all charts are drawn on the same rectangle
            devYTop += devGraphHeight;
        }

        drawingData.setScaleY(graphScaleY);

        drawingData.setDevYBottom(devYTop);
        drawingData.setDevYTop(devYTop - devGraphHeight);

        drawingData.setGraphYBottom(graphMinValue);
        drawingData.setGraphYTop(graphMaxValue);

        drawingData.devGraphHeight = devGraphHeight;
        drawingData.setDevSliderHeight(_devSliderBarHeight);

        final ArrayList<ChartUnit> unitList = drawingData.getYUnits();
        int graphValue = graphMinValue;
        int maxUnits = 0;
        final int valueDivisor = yData.getValueDivisor();

        // loop: create unit label for all units
        while (graphValue <= graphMaxValue) {

            final String unitLabel = net.tourbook.chart.Util.formatValue(graphValue, unitType, valueDivisor, false);
            final boolean isMajorValue = graphValue % majorValue == 0;

            unitList.add(new ChartUnit(graphValue, unitLabel, isMajorValue));

            // prevent endless loops when the unit is 0
            if (graphValue == graphMaxValue || maxUnits++ > 1000) {
                break;
            }

            graphValue += graphUnit;
        }

        if (unitType == ChartDataSerie.AXIS_UNIT_HOUR_MINUTE_24H && graphValue > graphMaxValue) {
            unitList.add(new ChartUnit(graphMaxValue, UI.EMPTY_STRING));
        }

    }

    private void createHistoryMonthUnits_Months(final ArrayList<ChartUnit> xUnits, final int[] historyMonths,
            final long graphValue, final int skippedMonths, final int majorMonth, final boolean isShowLabel) {

        int allMonthDays = 0;

        for (int monthIndex = 0; monthIndex < historyMonths.length; monthIndex++) {

            final int monthDays = historyMonths[monthIndex];

            allMonthDays += monthDays;

            if (monthIndex % skippedMonths != 0) {
                // skip months which are not displayed
                continue;
            }

            final int monthValueDays = allMonthDays - monthDays;
            final int monthValue = monthValueDays * DAY_IN_SECONDS;

            final String valueLabel = isShowLabel ? _monthLabels[monthIndex] : UI.EMPTY_STRING;
            final boolean isMajorValue = majorMonth == 0 ? false : monthIndex % majorMonth == 0;

            xUnits.add(new ChartUnit(//
                    graphValue + monthValue + DAY_IN_SECONDS - 1, //
                    valueLabel, isMajorValue)
            //
            );
        }
    }

    private void createHistoryUnits(final int firstYear, int lastYear) {

        /*
         * add an additional year because at the year end, a history chart displays also the next
         * year which caused an outOfBound exception when testing this app at 28.12.2012
         */
        lastYear += 1;

        final int numberOfYears = lastYear - firstYear + 1;

        _historyYears = new int[numberOfYears];
        _historyMonths = new int[numberOfYears][12];
        _historyDOY = new int[numberOfYears];

        int yearIndex = 0;

        DateTime currentYear = new DateTime().withDate(firstYear - 1, 1, 1);

        for (int currentYearNo = firstYear; currentYearNo <= lastYear; currentYearNo++) {

            currentYear = currentYear.plusYears(1);

            _historyYears[yearIndex] = currentYearNo;

            int yearDOY = 0;

            // get number of days for each month
            for (int monthIndex = 0; monthIndex < 12; monthIndex++) {

                final int monthDays = currentYear.withMonthOfYear(monthIndex + 1).dayOfMonth().getMaximumValue();

                _historyMonths[yearIndex][monthIndex] = monthDays;

                yearDOY += monthDays;
            }

            _historyDOY[yearIndex] = yearDOY;

            yearIndex++;
        }
    }

    private void createMonthEqualUnits(final GraphDrawingData drawingData, final long devGraphWidth,
            final int allUnitsSize, final int numberOfYears) {

        final ArrayList<ChartUnit> xUnits = drawingData.getXUnits();
        final boolean isDrawUnits[] = new boolean[allUnitsSize];

        /*
         * create month labels depending on the available width for a unit
         */
        final int devYearWidth = (int) (devGraphWidth / numberOfYears);
        if (devYearWidth >= _devAllMonthLabelWidth) {

            // all month labels can be displayed

            for (int monthIndex = 0; monthIndex < allUnitsSize; monthIndex++) {
                xUnits.add(new ChartUnit(monthIndex, _monthLabels[monthIndex % 12]));
                isDrawUnits[monthIndex] = true;
            }

        } else if (devYearWidth >= _devAllMonthLabelWidth / 3) {

            // every second month label can be displayed

            for (int monthIndex = 0; monthIndex < allUnitsSize; monthIndex++) {

                final String monthLabel = monthIndex % 3 == 0 //
                        ? _monthLabels[monthIndex % 12]
                        : UI.EMPTY_STRING;

                xUnits.add(new ChartUnit(monthIndex, monthLabel));
                isDrawUnits[monthIndex] = monthIndex % 3 == 0;
            }

        } else if (devYearWidth >= _devAllMonthShortLabelWidth / 3) {

            // every second month short label can be displayed

            for (int monthIndex = 0; monthIndex < allUnitsSize; monthIndex++) {

                final String monthLabel = monthIndex % 3 == 0 //
                        ? _monthShortLabels[monthIndex % 12]
                        : UI.EMPTY_STRING;

                xUnits.add(new ChartUnit(monthIndex, monthLabel));
                isDrawUnits[monthIndex] = monthIndex % 3 == 0;
            }

        } else if (devYearWidth >= _devAllMonthShortLabelWidth / 6) {

            // every second month short label can be displayed

            for (int monthIndex = 0; monthIndex < allUnitsSize; monthIndex++) {

                final String monthLabel = monthIndex % 6 == 0 //
                        ? _monthShortLabels[monthIndex % 12]
                        : UI.EMPTY_STRING;

                xUnits.add(new ChartUnit(monthIndex, monthLabel));
                isDrawUnits[monthIndex] = monthIndex % 3 == 0;
            }

        } else {

            // width is too small to display month labels, display nothing

            for (int monthIndex = 0; monthIndex < allUnitsSize; monthIndex++) {
                xUnits.add(new ChartUnit(monthIndex, UI.EMPTY_STRING));
            }
        }

        // set state that the overlap is not checked again
        drawingData.setIsXUnitOverlapChecked(true);

        drawingData.setIsDrawUnit(isDrawUnits);
    }

    /**
     * Create the labels for the months by using the days to scale the x-axis
     * 
     * @param drawingData
     * @param devGraphWidth
     * @param years
     * @param yearDays
     *            Number of days in one year
     */
    private void createMonthUnequalUnits(final GraphDrawingData drawingData, final long devGraphWidth,
            final int[] years, final int[] yearDays) {

        /*
         * multiple years can have different number of days but we assume that each year has the
         * same number of days to make it simpler
         */

        final int numberOfYears = years.length;
        final int numberOfMonths = numberOfYears * 12; // number of units

        final boolean isDrawUnits[] = new boolean[numberOfMonths];

        /*
         * create list with the day number for all years and months
         */
        final int[] daysForAllUnits = new int[numberOfMonths];
        int allDays = 0;
        for (int yearIndex = 0; yearIndex < numberOfYears; yearIndex++) {

            // create month units
            for (int monthIndex = 0; monthIndex < 12; monthIndex++) {

                _calendar.set(years[yearIndex], monthIndex, 1);
                final int firstDayInMonth = _calendar.get(Calendar.DAY_OF_YEAR) - 1;

                final int unitIndex = yearIndex * 12 + monthIndex;
                daysForAllUnits[unitIndex] = allDays + firstDayInMonth;
            }

            allDays += yearDays[yearIndex];
        }

        final ArrayList<ChartUnit> xUnits = drawingData.getXUnits();

        /*
         * create month labels depending on the available width for a unit
         */
        final int devYearWidth = (int) (devGraphWidth / numberOfYears);
        if (devYearWidth >= _devAllMonthLabelWidth) {

            // all month labels can be displayed

            for (int monthIndex = 0; monthIndex < numberOfMonths; monthIndex++) {

                final int unitValue = daysForAllUnits[monthIndex];

                xUnits.add(new ChartUnit(unitValue, _monthLabels[monthIndex % 12]));

                isDrawUnits[monthIndex] = true;
            }

        } else if (devYearWidth >= _devAllMonthLabelWidth / 3) {

            // every second month label can be displayed

            for (int monthIndex = 0; monthIndex < numberOfMonths; monthIndex++) {

                final String monthLabel = monthIndex % 3 == 0 //
                        ? _monthLabels[monthIndex % 12]
                        : UI.EMPTY_STRING;

                final int unitValue = daysForAllUnits[monthIndex];

                xUnits.add(new ChartUnit(unitValue, monthLabel));

                //            isDrawUnits[monthIndex] = monthIndex % 3 == 0;
                isDrawUnits[monthIndex] = true;
            }

        } else if (devYearWidth >= _devAllMonthShortLabelWidth / 3) {

            // every second month short label can be displayed

            for (int monthIndex = 0; monthIndex < numberOfMonths; monthIndex++) {

                final String monthLabel = monthIndex % 3 == 0 //
                        ? _monthShortLabels[monthIndex % 12]
                        : UI.EMPTY_STRING;

                xUnits.add(new ChartUnit(daysForAllUnits[monthIndex], monthLabel));
                isDrawUnits[monthIndex] = monthIndex % 3 == 0;
            }

        } else if (devYearWidth >= _devAllMonthShortLabelWidth / 6) {

            // every second month short label can be displayed

            for (int monthIndex = 0; monthIndex < numberOfMonths; monthIndex++) {

                final String monthLabel = monthIndex % 6 == 0 //
                        ? _monthShortLabels[monthIndex % 12]
                        : UI.EMPTY_STRING;

                xUnits.add(new ChartUnit(daysForAllUnits[monthIndex], monthLabel));
                isDrawUnits[monthIndex] = monthIndex % 3 == 0;
            }

        } else {

            // width is too small to display month labels, display nothing

            for (int monthIndex = 0; monthIndex < numberOfMonths; monthIndex++) {
                xUnits.add(new ChartUnit(daysForAllUnits[monthIndex], UI.EMPTY_STRING));
            }
        }

        // set state that the overlap is not checked again
        drawingData.setIsXUnitOverlapChecked(true);

        drawingData.setIsDrawUnit(isDrawUnits);

        //      // shorten the unit when there is not enough space to draw the full unit name
        //      final GC gc = new GC(this);
        //      final int monthLength = gc.stringExtent(_monthLabels[0]).x;
        //      final boolean useShortUnitLabel = monthLength > (devGraphWidth / allUnitsSize) * 0.9;
        //      gc.dispose();
        //
        //      /*
        //       * create month units for all years
        //       */
        //      for (int yearIndex = 0; yearIndex < years.length; yearIndex++) {
        //
        //         final int year = years[yearIndex];
        //
        //         // create month units
        //         for (int month = 0; month < 12; month++) {
        //
        //            _calendar.set(year, month, 1);
        //            final int firstDayInMonth = _calendar.get(Calendar.DAY_OF_YEAR) - 1;
        //
        //            String monthLabel = _monthLabels[month];
        //            if (useShortUnitLabel) {
        //               monthLabel = monthLabel.substring(0, 1);
        //            }
        //
        //            units.add(new ChartUnit(allDays + firstDayInMonth, monthLabel));
        //         }
        //
        //         allDays += yearDays[yearIndex];
        //      }
    }

    /**
     * set the {@link SynchConfiguration} when this chart is the source for the synched chart
     */
    private SynchConfiguration createSynchConfig() {

        final ChartDataXSerie xData = _chartDataModel.getXData();

        final int markerValueIndexStart = xData.getSynchMarkerStartIndex();
        final int markerValueIndexEnd = xData.getSynchMarkerEndIndex();

        if (markerValueIndexStart == -1) {

            // disable chart synchronization
            _synchConfigOut = null;
            return null;
        }

        /*
         * create synch configuration data
         */

        final double[] xValues = xData.getHighValuesDouble()[0];
        final double markerStartValue = xValues[Math.min(markerValueIndexStart, xValues.length - 1)];
        final double markerEndValue = xValues[Math.min(markerValueIndexEnd, xValues.length - 1)];

        final double valueDiff = markerEndValue - markerStartValue;
        final double lastValue = xValues[xValues.length - 1];

        final float devVirtualGraphImageWidth = componentGraph.getXXDevGraphWidth();
        final float devGraphImageXOffset = componentGraph.getXXDevViewPortLeftBorder();

        final double devOneValueSlice = devVirtualGraphImageWidth / lastValue;

        final float devMarkerWidth = (int) (valueDiff * devOneValueSlice);
        final float devMarkerStartPos = (int) (markerStartValue * devOneValueSlice);
        final float devMarkerOffset = (int) (devMarkerStartPos - devGraphImageXOffset);

        final int devVisibleChartWidth = getDevVisibleChartWidth();

        final float markerWidthRatio = devMarkerWidth / devVisibleChartWidth;
        final float markerOffsetRatio = devMarkerOffset / devVisibleChartWidth;

        // ---------------------------------------------------------------------------------------

        final SynchConfiguration synchConfig = new SynchConfiguration(_chartDataModel, devMarkerWidth,
                devMarkerOffset, markerWidthRatio, markerOffsetRatio);

        return synchConfig;
    }

    public ChartComponentAxis getAxisLeft() {
        return componentAxisLeft;
    }

    public ChartComponentAxis getAxisRight() {
        return componentAxisRight;
    }

    ChartComponentGraph getChartComponentGraph() {
        return componentGraph;
    }

    ChartDataModel getChartDataModel() {
        return _chartDataModel;
    }

    ChartDrawingData getChartDrawingData() {
        return _chartDrawingData;
    }

    private int getDevChartHeightWithoutTrim() {

        return _visibleGraphRect.height //
                - _devMarginTop - _devXTitleBarHeight - _devXAxisHeight;
    }

    int getDevChartMarginBottom() {
        return _devXAxisHeight;
    }

    int getDevChartMarginTop() {
        return _devMarginTop //
                + _devXTitleBarHeight + _devSliderBarHeight;
    }

    /**
     * @return Returns the visible chart graph height
     */
    int getDevVisibleChartHeight() {

        if (_visibleGraphRect == null) {
            return 100;
        }

        return _visibleGraphRect.height;
    }

    /**
     * @return Returns the visible chart width
     */
    int getDevVisibleChartWidth() {

        if (_visibleGraphRect == null) {
            return 100;
        }

        return _visibleGraphRect.width;
    }

    int getMarginBottomStartingFromTop() {

        return _visibleGraphRect.height //
                - _devXAxisHeight;
    }

    /**
     * Get size in pixel for the month labels and shortcuts
     */
    private void getMonthLabelWidth() {

        final GC gc = new GC(this);
        {
            _devAllMonthLabelWidth = 0;
            _devAllMonthShortLabelWidth = 0;

            for (int monthIndex = 0; monthIndex < _monthLabels.length; monthIndex++) {
                _devAllMonthLabelWidth += gc.stringExtent(_monthLabels[monthIndex]).x + 6;
                _devAllMonthShortLabelWidth += gc.stringExtent(_monthShortLabels[monthIndex]).x + 12;
            }

            _devYearLabelWidth += gc.stringExtent("2222").x + 0;//$NON-NLS-1$
        }
        gc.dispose();
    }

    int getYAxisWidthLeft() {
        return getAxisLeft().getSize().x;
    }

    /**
     * Resize handler for all components, computes the chart when the chart data or the client area
     * has changed or when the chart was zoomed
     */
    boolean onResize() {

        if (isDisposed() || _chartDataModel == null || getClientArea().width == 0) {
            return false;
        }

        // compute the visual size of the graph
        setVisibleGraphRect();

        if (setWidthToSynchedChart() == false) {

            // chart is not synchronized, compute the 'normal' graph width
            componentGraph.updateGraphSize();
        }

        // compute the chart data
        _chartDrawingData = createDrawingData();

        // notify components about the new configuration
        componentGraph.setDrawingData(_chartDrawingData);

        // resize the sliders after the drawing data have changed and the new
        // chart size is saved
        componentGraph.updateSlidersOnResize();

        // resize the axis
        componentAxisLeft.setDrawingData(_chartDrawingData, true);
        componentAxisRight.setDrawingData(_chartDrawingData, false);

        componentGraph.updateChartSize();

        // synchronize chart
        final SynchConfiguration synchConfig = createSynchConfig();
        if (synchConfig != null) {
            synchronizeChart(synchConfig);
        }

        return true;
    }

    void selectBarItem(final Event event) {

        _keyDownCounter[0]++;

        final int[] selectedIndex = new int[] { Chart.NO_BAR_SELECTION };

        switch (event.keyCode) {
        case SWT.ARROW_RIGHT:
            selectedIndex[0] = componentGraph.selectBarItemNext();
            break;
        case SWT.ARROW_LEFT:
            selectedIndex[0] = componentGraph.selectBarItemPrevious();
            break;
        }

        // fire the event when the selection has changed
        if (selectedIndex[0] != Chart.NO_BAR_SELECTION) {

            /*
             * delay the change event when the key down was pressed several times
             */
            final Display display = Display.getCurrent();
            display.asyncExec(new Runnable() {
                public void run() {
                    display.timerExec(BAR_SELECTION_DELAY_TIME, new Runnable() {

                        final int __runnableKeyDownCounter = _keyDownCounter[0];

                        public void run() {
                            if (__runnableKeyDownCounter == _keyDownCounter[0]
                                    && __runnableKeyDownCounter != _lastKeyDownCounter[0]) {

                                /*
                                 * prevent redoing it, this happened when the selectNext/Previous
                                 * Method took a long time when the chart was drawn
                                 */
                                _lastKeyDownCounter[0] = __runnableKeyDownCounter;

                                _chart.fireBarSelectionEvent(0, selectedIndex[0]);
                            }
                        }
                    });
                }
            });
        }
    }

    void setErrorMessage(final String errorMessage) {
        this.errorMessage = errorMessage;
    }

    /**
     * set the chart data model and redraw the chart
     * 
     * @param chartModel
     * @param isShowAllData
     */
    void setModel(final ChartDataModel chartModel, final boolean isShowAllData) {

        _chartDataModel = chartModel;

        /*
         * when data model has changed, update the visible y-values to use the full visible area for
         * drawing the chart
         */
        final ChartType chartType = _chartDataModel.getChartType();
        if (isShowAllData && (chartType == ChartType.LINE || chartType == ChartType.LINE_WITH_BARS
                || chartType == ChartType.HISTORY)) {

            componentGraph.updateVisibleMinMaxValues();
        }

        if (onResize()) {
            /*
             * resetting the sliders require that the drawing data are created, this is done in the
             * onResize method
             */
            if (_devSliderBarHeight > 0) {
                componentGraph.resetSliders();
            }
        }
    }

    /**
     * @param isSliderVisible
     */
    void setSliderVisible(final boolean isSliderVisible) {

        _devSliderBarHeight = isSliderVisible ? SLIDER_BAR_HEIGHT : 0;

        componentGraph.setXSliderVisible(isSliderVisible);
    }

    /**
     * Set's a {@link SynchConfiguration}, this chart will then be sychronized with the chart which
     * sets the synch config
     * 
     * @param fSynchConfigSrc
     *            the xMarkerPosition to set
     */
    void setSynchConfig(final SynchConfiguration synchConfigIn) {

        _synchConfigSrc = synchConfigIn;

        onResize();
    }

    private void setVisibleGraphRect() {

        final ArrayList<ChartDataYSerie> yDataList = _chartDataModel.getYData();
        boolean isYTitle = false;

        // loop all graphs - find the title for the y-axis
        for (final ChartDataYSerie yData : yDataList) {
            if (yData.getYTitle() != null || yData.getUnitLabel() != null) {
                isYTitle = true;
                break;
            }
        }

        if (isYTitle) {

            _yAxisWidthLeftWithTitle = _yAxisWidthLeft + TITLE_BAR_HEIGHT;

            final GridData gl = (GridData) componentAxisLeft.getLayoutData();
            gl.widthHint = _yAxisWidthLeftWithTitle;

            // relayout after the layout size has been changed
            layout();
        }

        // set the visible graph size
        final Rectangle clientRect = getClientArea();
        final int devGraphWidth = clientRect.width - (_yAxisWidthLeftWithTitle + _yAxisWidthRight) - 0;

        _visibleGraphRect = new Rectangle(_yAxisWidthLeftWithTitle, 0, devGraphWidth, clientRect.height);
    }

    /**
     * adjust the graph width to the synched chart
     * 
     * @return Returns <code>true</code> when the graph width was set
     */
    private boolean setWidthToSynchedChart() {

        final ChartDataXSerie xData = _chartDataModel.getXData();
        final int markerStartIndex = xData.getSynchMarkerStartIndex();
        final int markerEndIndex = xData.getSynchMarkerEndIndex();

        // check if synchronization is disabled
        if (_synchConfigSrc == null || markerStartIndex == -1) {
            return false;
        }

        // set min/max values from the source synched chart into this chart
        _synchConfigSrc.getYDataMinMaxKeeper().setMinMaxValues(_chartDataModel);

        final double[] xValues = xData.getHighValuesDouble()[0];
        final double markerValueStart = xValues[markerStartIndex];

        final double valueDiff = xValues[markerEndIndex] - markerValueStart;
        final double valueLast = xValues[xValues.length - 1];

        final int devVisibleChartWidth = getDevVisibleChartWidth();

        final double xxDevGraphWidth;
        final int xxDevViewPortOffset;
        final double graphZoomRatio;

        switch (_chart._synchMode) {
        case Chart.SYNCH_MODE_BY_SCALE:

            // get marker data from the synch source
            final float markerWidthRatio = _synchConfigSrc.getMarkerWidthRatio();
            final float markerOffsetRatio = _synchConfigSrc.getMarkerOffsetRatio();

            // virtual graph width
            final float devMarkerWidth = devVisibleChartWidth * markerWidthRatio;
            final double devOneValueSlice = devMarkerWidth / valueDiff;
            xxDevGraphWidth = devOneValueSlice * valueLast;

            // graph offset
            final float devMarkerOffset = devVisibleChartWidth * markerOffsetRatio;
            final double devMarkerStart = devOneValueSlice * markerValueStart;
            xxDevViewPortOffset = (int) (devMarkerStart - devMarkerOffset);

            // zoom ratio
            graphZoomRatio = xxDevGraphWidth / devVisibleChartWidth;

            componentGraph.setGraphSize((int) xxDevGraphWidth, xxDevViewPortOffset, graphZoomRatio);

            return true;

        case Chart.SYNCH_MODE_BY_SIZE:

            // get marker data from the synch source
            final float synchSrcDevMarkerWidth = _synchConfigSrc.getDevMarkerWidth();
            final float synchSrcDevMarkerOffset = _synchConfigSrc.getDevMarkerOffset();

            // virtual graph width
            xxDevGraphWidth = valueLast / valueDiff * synchSrcDevMarkerWidth;

            // graph offset
            final int devLeftSynchMarkerPos = (int) (markerValueStart / valueLast * xxDevGraphWidth);
            xxDevViewPortOffset = (int) (devLeftSynchMarkerPos - synchSrcDevMarkerOffset);

            // zoom ratio
            graphZoomRatio = xxDevGraphWidth / devVisibleChartWidth;

            componentGraph.setGraphSize((int) xxDevGraphWidth, xxDevViewPortOffset, graphZoomRatio);

            return true;

        default:
            break;
        }

        return false;
    }

    /**
     * set the x-sliders to a new position, this is done from a selection provider
     * 
     * @param sliderPosition
     */
    void setXSliderPosition(final SelectionChartXSliderPosition sliderPosition) {

        if (sliderPosition == null) {
            /*
             * nothing to do when the position was not set, this can happen when the chart was not
             * yet created
             */
            return;
        }

        if (_chartDataModel == null) {
            return;
        }

        final ChartXSlider leftSlider = componentGraph.getLeftSlider();
        final ChartXSlider rightSlider = componentGraph.getRightSlider();

        final int slider0ValueIndex = sliderPosition.getBeforeLeftSliderIndex();
        final int slider1ValueIndex = sliderPosition.getLeftSliderValueIndex();
        final int slider2ValueIndex = sliderPosition.getRightSliderValueIndex();

        final double[] xValues = _chartDataModel.getXData()._highValuesDouble[0];
        final boolean isCenterSliderPosition = sliderPosition.isCenterSliderPosition();

        // move left slider
        if (slider1ValueIndex == SelectionChartXSliderPosition.SLIDER_POSITION_AT_CHART_BORDER) {

            componentGraph.setXSliderValueIndex(//
                    leftSlider, 0, isCenterSliderPosition);

        } else if (slider1ValueIndex != SelectionChartXSliderPosition.IGNORE_SLIDER_POSITION) {

            componentGraph.setXSliderValueIndex(//
                    leftSlider, slider1ValueIndex, isCenterSliderPosition);
        }

        if (slider0ValueIndex != SelectionChartXSliderPosition.IGNORE_SLIDER_POSITION) {

            // move second slider before the first slider

            componentGraph.setXSliderValueIndex(rightSlider, slider0ValueIndex, isCenterSliderPosition);

        } else {

            // move right slider
            if (slider2ValueIndex == SelectionChartXSliderPosition.SLIDER_POSITION_AT_CHART_BORDER) {

                componentGraph.setXSliderValueIndex(//
                        rightSlider, xValues.length - 1, isCenterSliderPosition);

            } else if (slider2ValueIndex != SelectionChartXSliderPosition.IGNORE_SLIDER_POSITION) {

                componentGraph.setXSliderValueIndex(//
                        rightSlider, slider2ValueIndex, isCenterSliderPosition);
            }
        }

        componentGraph.redraw();
    }

    private void synchronizeChart(final SynchConfiguration newSynchConfigOut) {

        boolean fireEvent = false;

        if (_synchConfigOut == null) {
            // synch new config
            fireEvent = true;
        } else if (_synchConfigOut.isEqual(newSynchConfigOut) == false) {
            // synch when config changed
            fireEvent = true;
        }

        if (fireEvent) {

            // set new synch config
            _synchConfigOut = newSynchConfigOut;

            _chart.synchronizeChart();
        }
    }

    void updateCustomLayers() {

        if (_chartDrawingData == null) {
            return;
        }

        int graphIndex = 0;
        final ArrayList<GraphDrawingData> graphDrawingData = _chartDrawingData.graphDrawingData;

        /*
         * set custom layers from the data model into the drawing data
         */

        for (final ChartDataYSerie yData : _chartDataModel.getYData()) {
            graphDrawingData.get(graphIndex++).getYData()
                    .setCustomForegroundLayers(yData.getCustomForegroundLayers());
        }

        componentGraph.updateCustomLayers();
    }

}