/* * Copyright 2012 Axel Winkler, Daniel Dunér * * This file is part of Daxplore Presenter. * * Daxplore Presenter is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 2.1 of the License, or * (at your option) any later version. * * Daxplore Presenter 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 Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with Daxplore Presenter. If not, see <http://www.gnu.org/licenses/>. */ package org.daxplore.presenter.chart.display; import java.util.LinkedList; import java.util.List; import org.daxplore.presenter.chart.data.QueryResult; import org.daxplore.presenter.chart.data.QueryResultCountCompare; import org.daxplore.presenter.chart.resources.ChartConfig; import org.daxplore.presenter.chart.resources.ChartTexts; import org.daxplore.presenter.client.json.UITexts; import org.daxplore.presenter.shared.QueryDefinition; import org.daxplore.presenter.shared.QueryDefinition.QueryFlag; /** * A bar chart that compares two data sets, displaying frequencies in different * groups. * * <p>The comparative version of the bar chart works like the standard bar * chart, except for a few details. For a description of the shared * functionality, see {@link BarChart}.</p> * * <p>In the comparative bar chart, each group displays to data sets. One of the * sets are called primary and one secondary. The primary data set is shown in * front in clear visible way, overlapping the secondary bars which are put in * the background. Each bar in the primary data set overlaps its corresponding * bar in the secondary data set, allowing for a direct comparison between the * two data sets.</p> * * @see BarChart * @see QueryInterface * @see QueryResult */ public class BarChartCompare extends BarChart { /** * The distance between the bar-pairs in a group. * * <p>Measured in GChart's internal distance system. */ private final static double internalGroupSpacing = 0.2; /** * How far the secondary bar is shifted to the right, relative to it's * primary bar. * * <p>Measured in GChart's internal distance system. */ private final static double secondaryBarShift = 0.5; /** * A list of all the secondary bars. */ private LinkedList<BarChartBarSecondary> barListSecondary; /** * Create a new bar chart that compare data sets. * * <p>A query is given, that contains all the data needed to draw the * barchart. When the query has loaded all the needed data the method * addData will be called, which finalizes the construction of the * chart.</p> * * @param query * The query that this chart will display. */ protected BarChartCompare(ChartTexts chartTexts, ChartConfig chartConfig, UITexts uiTexts, QueryDefinition queryDefinition, boolean printerMode) { super(chartTexts, chartConfig, uiTexts, queryDefinition, printerMode); xTickMaxCharacterCount = 19; // Twice the number of bars, compared to the basic bar chart. barCount *= 2; betweenGroupsTickCurveIndex = barCount; paddingBarIndex = betweenGroupsTickCurveIndex + groupCount; groupWidth = questionOptionCount * (1 + secondaryBarShift) + (questionOptionCount - 1) * internalGroupSpacing; groupDistance = groupWidth + groupSpacing; addStyleDependentName("compare"); } @Override protected void createCurves(List<String> questionOptionTexts, boolean printerMode) { /* * Lists are created here to make sure they exist when the * super-constructor calls the overridden version of * createCurvesAndLegend. */ barListPrimary = new LinkedList<>(); barListSecondary = new LinkedList<>(); for (int groupIndex = 0; groupIndex < groupCount; groupIndex++) { for (int questionIndex = 0; questionIndex < questionOptionCount; questionIndex++) { addCurve(); Curve curveSecondary = getCurve(); addCurve(); Curve curvePrimary = getCurve(); AnnotationLocation hoverLocation = AnnotationLocation.SOUTH; if(groupCount>1 && groupIndex==0){ hoverLocation = AnnotationLocation.SOUTHEAST; } else if(groupCount>1 && groupIndex==groupCount-1) { hoverLocation = AnnotationLocation.SOUTHWEST; } addPrimaryBar(new BarChartBarPrimary(chartTexts, curvePrimary, getColorSet(questionIndex), printerMode, hoverLocation)); addSecondaryBar(new BarChartBarSecondary(chartTexts, curveSecondary, getColorSet(questionIndex), printerMode, hoverLocation)); } } for (int i = 0; i < groupCount; i++) { addBetweenGroupsTickCurve(); } addPaddingCurve(); } /** * When a new secondary bar is created, add it here. * * <p>It is important to add the curves in left-to-right order. By keeping * track of the index of the bar, we can find it when we want to add data to * the curve of a specific index. It also makes it possible to find the bar * that belongs to a curve that is being hovered.</p> * * @param bar * The bar to be stored in the chart */ private void addSecondaryBar(BarChartBarSecondary bar) { barListSecondary.add(bar); } /** * Get the secondary bar at a specific position in the chart. * * <p>The bars are ordered from left to right. Each index will have one * primary bar and one secondary bar.</p> * * @param index * The position of the bar in the chart. * @return The primary bar at the given position. * * @see getBarSecondary */ private ChartBar getBarSecondary(int index) { return barListSecondary.get(index); } @Override protected ChartBar getBar(Curve curve) { for (ChartBar column : barListPrimary) { if (column.getCurve() == curve) { return column; } } for (ChartBar column : barListSecondary) { if (column.getCurve() == curve) { return column; } } return null; } /** * Add the data and complete the construction of the chart. */ public void addData(QueryResultCountCompare queryResult) { List<String> perspectiveOptionTexts = queryDefinition.getPerspectiveOptionTexts(); currentPosition = 1 + groupSpacing / 2; currentGroup = 0; for (int perspectiveOption : usedPerspectiveOptions) { if (currentGroup > 0) { drawBetweenGroupsTick(); } if (queryResult.hasData(perspectiveOption) && queryResult.getPopulation(perspectiveOption)!=0) { //TODO temporary hack, handle cut-off properly in producer drawBarGroup(perspectiveOptionTexts.get(perspectiveOption), queryResult.getPopulation(perspectiveOption), queryResult.getCountDataPercentages(perspectiveOption), queryResult.getPopulationSecondary(perspectiveOption), queryResult.getCountDataPercentagesSecondary(perspectiveOption)); } else { drawMissingBarGroup(perspectiveOptionTexts.get(perspectiveOption)); } currentPosition += groupDistance; currentGroup++; } if (queryDefinition.hasFlag(QueryFlag.TOTAL)) { if (currentGroup > 0) { drawBetweenGroupsTick(); } String totalText = chartTexts.compareWithAll(); if (queryResult.hasTotalDataItemData()&& queryResult.getTotalPopulation()!=0) { //TODO temporary hack, handle cut-off properly in producer drawBarGroup(totalText, queryResult.getTotalPopulation(), queryResult.getTotalCountDataPercentages(), queryResult.getTotalPopulationSecondary(), queryResult.getTotalCountDataPercentagesSecondary()); } else { drawMissingBarGroup(totalText); } currentPosition += groupDistance; currentGroup++; } currentPosition -= (1 + groupSpacing / 2); drawPaddingBar(); setYAxis(maxValue); update(); setVisible(true); } /** * Draw a group of bars. * * <p>The group will be drawn at the position given by currentPosition. Each * bar in the group has a local index, beginning at 0. The height of the bar * at a specific index is given by the height of the value at the * corresponding index in the percentageData array.</p> * * @param groupName * The name of the group, to be displayed under the x-axis tick. * @param groupSizePrimary * The number of people in the primary group, to be displayed * under the x-axis tick. * @param percentageDataPrimary * An array containing the primary bar data. * @param groupSizeSecondary * The number of people in the secondary group, to be displayed * under the x-axis tick. * @param percentageDataSecondary * An array containing the secondary bar data. */ private void drawBarGroup(String groupName, int groupSizePrimary, double[] percentageDataPrimary, int groupSizeSecondary, double[] percentageDataSecondary) { List<String> questionOptionTexts = queryDefinition.getQuestionOptionTexts(); for (int dataIndex = 0; dataIndex < questionOptionCount; dataIndex++) { BarChartBarPrimary primaryBar = (BarChartBarPrimary) getBarPrimary(currentGroup * questionOptionCount + dataIndex); Curve curve = primaryBar.getCurve(); if (dataIndex < percentageDataPrimary.length) { curve.addPoint(currentPosition + dataIndex * (1 + secondaryBarShift + internalGroupSpacing), percentageDataPrimary[dataIndex]); primaryBar.setHoverTextComparative(uiTexts.timepointPrimary(), percentageDataPrimary[dataIndex], questionOptionTexts.get(dataIndex)); if (percentageDataPrimary[dataIndex] > maxValue) { maxValue = percentageDataPrimary[dataIndex]; } } BarChartBarSecondary secondaryBar = (BarChartBarSecondary) getBarSecondary(currentGroup * questionOptionCount + dataIndex); curve = secondaryBar.getCurve(); if (groupSizeSecondary > 0 && dataIndex < percentageDataSecondary.length) { curve.addPoint(currentPosition + dataIndex * (1 + secondaryBarShift + internalGroupSpacing) + secondaryBarShift, percentageDataSecondary[dataIndex]); secondaryBar.setHoverTextComparative(uiTexts.timepointSecondary(), percentageDataSecondary[dataIndex], questionOptionTexts.get(dataIndex)); if (percentageDataSecondary[dataIndex] > maxValue) { maxValue = percentageDataSecondary[dataIndex]; } } } // TODO calculate offset from widgets? xTickMaxCharacterCount = Math.max(xTickMaxCharacterCount, groupName.length()); String tickText; if (groupSizeSecondary > 0) { tickText = chartTexts.compareTick(groupName, uiTexts.timepointPrimary(), groupSizePrimary, uiTexts.timepointSecondary(), groupSizeSecondary); } else { tickText = chartTexts.compareMissingSecondaryTick(groupName, uiTexts.timepointPrimary(), groupSizePrimary, uiTexts.timepointSecondary(), chartConfig.respondentCountCutoff()); xTickMaxCharacterCount = Math.max(xTickMaxCharacterCount, 23); } getXAxis().addTick(currentPosition + groupWidth / 2 - 1, tickText); } /** * Draws a group for which there is no data. * * <p>When a group is too small there is no data to display. Call this * method to add a tick and explanatory text, where the group should have * been.</p> * * @param groupName */ private void drawMissingBarGroup(String groupName) { String tickText = chartTexts.compareMissingTick(groupName, chartConfig.respondentCountCutoff()); getXAxis().addTick(currentPosition + groupWidth / 2 - 1, tickText); } /** * {@inheritDoc} */ @Override public void setChartSizeSmart(int width, int height) { setChartSize(width, height); setChartSize(Math.max(2 * width - getXChartSizeDecorated() - 5, 0), Math.max(2 * height - getYChartSizeDecorated() - 15, 0)); update(); } /** * {@inheritDoc} */ @Override public int getMinWidth() { double groupMinWidth = Math.max(xTickMaxCharacterCount * 7.5, questionOptionCount * 30); return (int) (80 + groupMinWidth * groupCount); } }