| | |
| | | import java.lang.reflect.Field; |
| | | import java.lang.reflect.Modifier; |
| | | import java.math.BigDecimal; |
| | | import java.math.RoundingMode; |
| | | import java.text.DecimalFormat; |
| | | import java.text.NumberFormat; |
| | | import java.util.ArrayList; |
| | |
| | | private static final Color AXIS_COLOR = Color.BLACK; |
| | | private static final Color MUTED_TEXT_COLOR = new Color(102, 102, 102); |
| | | private static final double UNIT_LABEL_BAND = 58.0; |
| | | private static final double RIGHT_LABEL_BAND = 74.0; |
| | | private static final double RIGHT_LABEL_BAND = 140.0; |
| | | private static final double ITEM_LABEL_GAP = 10.0; |
| | | private static final float HALO_OUTLINE_WIDTH = 4.0f; |
| | | private static final int PERCENT_ITEM_LABEL_DECIMALS = 1; |
| | | private static final double LINE_CHART_HEIGHT_SCALE = 1.20; |
| | | private static final Color ITEM_LABEL_COLOR = new Color(55, 55, 55); |
| | | private static final Color[] SERIES_COLORS = { |
| | | new Color(192, 80, 77), |
| | |
| | | for (int i = 0; i < item.getValues().size(); i++) { |
| | | BigDecimal value = item.getValues().get(i); |
| | | if (value != null) { |
| | | xySeries.add(i, value.doubleValue()); |
| | | xySeries.add(i, displayValue(value, percentAxis, valueDecimals).doubleValue()); |
| | | } |
| | | } |
| | | dataset.addSeries(xySeries); |
| | |
| | | |
| | | NumberAxis rangeAxis = new NumberAxis(); |
| | | rangeAxis.setAutoRangeIncludesZero(false); |
| | | rangeAxis.setLowerMargin(0.12); |
| | | rangeAxis.setUpperMargin(0.16); |
| | | rangeAxis.setLowerMargin(0.04); |
| | | rangeAxis.setUpperMargin(0.07); |
| | | rangeAxis.setTickLabelFont(TICK_FONT); |
| | | rangeAxis.setLabelFont(LABEL_FONT); |
| | | rangeAxis.setTickLabelPaint(MUTED_TEXT_COLOR); |
| | |
| | | BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND)); |
| | | } |
| | | plot.setRenderer(renderer); |
| | | return render(chart, width, height, yAxisLabel, true, percentAxis, valueDecimals); |
| | | return render(chart, width, scaleLineChartHeight(height), |
| | | yAxisLabel, true, percentAxis, valueDecimals); |
| | | } |
| | | |
| | | public byte[] pieChart(String valueSuffix, List<ChartCategory> categories, |
| | |
| | | occupied.add(new Rectangle2D.Double(textX, baseline - metrics.getAscent(), |
| | | width, metrics.getHeight())); |
| | | } else { |
| | | horizontalCallouts.add(new HorizontalStackCallout(Math.min(startX, endX), |
| | | horizontalCallouts.add(new HorizontalStackCallout(row, Math.min(startX, endX), |
| | | Math.max(startX, endX), centerY, text, seriesColor(row))); |
| | | } |
| | | } else { |
| | |
| | | } |
| | | } |
| | | if (horizontal) { |
| | | drawHorizontalStackCallouts(graphics, horizontalCallouts, dataArea, metrics, occupied); |
| | | drawHorizontalStackCallouts(graphics, horizontalCallouts, dataArea, metrics, occupied, |
| | | rows >= 3); |
| | | } else { |
| | | for (int column = 0; column < columns; column++) { |
| | | drawStackCalloutColumn(graphics, image, callouts.get(column), column, columns, |
| | |
| | | private void drawHorizontalStackCallouts(Graphics2D graphics, |
| | | List<HorizontalStackCallout> items, |
| | | Rectangle2D dataArea, FontMetrics metrics, |
| | | List<Rectangle2D> occupied) { |
| | | List<Rectangle2D> occupied, |
| | | boolean packIntoColumns) { |
| | | if (items.isEmpty()) return; |
| | | Map<Long, List<HorizontalStackCallout>> rows = new LinkedHashMap<>(); |
| | | for (HorizontalStackCallout item : items) { |
| | |
| | | Collections.sort(row, new Comparator<HorizontalStackCallout>() { |
| | | @Override |
| | | public int compare(HorizontalStackCallout first, HorizontalStackCallout second) { |
| | | return Double.compare(first.getStartX(), second.getStartX()); |
| | | return Integer.compare(first.getSeriesIndex(), second.getSeriesIndex()); |
| | | } |
| | | }); |
| | | int widest = 0; |
| | | double columnX = 0.0; |
| | | for (HorizontalStackCallout item : row) { |
| | | widest = Math.max(widest, metrics.stringWidth(item.getText())); |
| | | columnX = Math.max(columnX, item.getEndX() + 8.0); |
| | | if (!packIntoColumns) { |
| | | int widest = 0; |
| | | double columnX = 0.0; |
| | | for (HorizontalStackCallout item : row) { |
| | | widest = Math.max(widest, metrics.stringWidth(item.getText())); |
| | | columnX = Math.max(columnX, item.getEndX() + 8.0); |
| | | } |
| | | // Keep the label column inside the reserved right margin. |
| | | columnX = Math.min(columnX, dataArea.getMaxX() + RIGHT_LABEL_BAND - widest); |
| | | int height = metrics.getHeight(); |
| | | double step = height + 3.0; |
| | | double centerY = row.get(0).getY(); |
| | | double top = centerY - (step * row.size()) / 2.0 + (step - height) / 2.0; |
| | | for (int i = 0; i < row.size(); i++) { |
| | | HorizontalStackCallout item = row.get(i); |
| | | int width = metrics.stringWidth(item.getText()); |
| | | double x = columnX; |
| | | double y = top + i * step; |
| | | graphics.setColor(item.getColor()); |
| | | graphics.setStroke(new BasicStroke(1.4f, BasicStroke.CAP_ROUND, |
| | | BasicStroke.JOIN_ROUND)); |
| | | graphics.drawLine((int) Math.round(item.getEndX()), (int) Math.round(item.getY()), |
| | | (int) Math.round(x - 3.0), (int) Math.round(y + height / 2.0)); |
| | | drawLabelWithHalo(graphics, item.getText(), |
| | | new Rectangle2D.Double(x, y, width, height), metrics, item.getColor()); |
| | | occupied.add(new Rectangle2D.Double(x - 2.0, y - 2.0, |
| | | width + 4.0, height + 4.0)); |
| | | } |
| | | continue; |
| | | } |
| | | // Keep the label column inside the reserved right margin. |
| | | columnX = Math.min(columnX, dataArea.getMaxX() + RIGHT_LABEL_BAND - widest); |
| | | // Small slices are labelled outside the bar. Packing those labels |
| | | // into two columns keeps each day's callouts inside its own row. |
| | | int columnCount = row.size() <= 1 ? 1 : 2; |
| | | int rowCount = (int) Math.ceil(row.size() / (double) columnCount); |
| | | double[] columnWidths = new double[columnCount]; |
| | | for (int i = 0; i < row.size(); i++) { |
| | | int column = columnCount == 1 ? 0 : i % columnCount; |
| | | columnWidths[column] = Math.max(columnWidths[column], |
| | | metrics.stringWidth(row.get(i).getText())); |
| | | } |
| | | double columnGap = 10.0; |
| | | double blockWidth = columnGap * (columnCount - 1); |
| | | for (double columnWidth : columnWidths) blockWidth += columnWidth; |
| | | double blockLeft = dataArea.getMaxX() + 8.0; |
| | | double blockRight = dataArea.getMaxX() + RIGHT_LABEL_BAND; |
| | | blockLeft += Math.max(0.0, (blockRight - blockLeft - blockWidth) / 2.0); |
| | | double[] columnCenters = new double[columnCount]; |
| | | double cursor = blockLeft; |
| | | for (int column = 0; column < columnCount; column++) { |
| | | columnCenters[column] = cursor + columnWidths[column] / 2.0; |
| | | cursor += columnWidths[column] + columnGap; |
| | | } |
| | | int height = metrics.getHeight(); |
| | | double step = height + 3.0; |
| | | double centerY = row.get(0).getY(); |
| | | double top = centerY - (step * row.size()) / 2.0 + (step - height) / 2.0; |
| | | double rowStep = height + 3.0; |
| | | double totalHeight = rowCount * height + (rowCount - 1) * (rowStep - height); |
| | | double top = row.get(0).getY() - totalHeight / 2.0; |
| | | for (int i = 0; i < row.size(); i++) { |
| | | HorizontalStackCallout item = row.get(i); |
| | | int column = columnCount == 1 ? 0 : i % columnCount; |
| | | int rowIndex = columnCount == 1 ? i : i / columnCount; |
| | | int itemsInRow = Math.min(columnCount, row.size() - rowIndex * columnCount); |
| | | int width = metrics.stringWidth(item.getText()); |
| | | double x = columnX; |
| | | double y = top + i * step; |
| | | graphics.setColor(item.getColor()); |
| | | graphics.setStroke(new BasicStroke(1.4f, BasicStroke.CAP_ROUND, |
| | | BasicStroke.JOIN_ROUND)); |
| | | graphics.drawLine((int) Math.round(item.getEndX()), (int) Math.round(item.getY()), |
| | | (int) Math.round(x - 3.0), (int) Math.round(y + height / 2.0)); |
| | | double centerX = itemsInRow == 1 && columnCount > 1 |
| | | ? blockLeft + blockWidth / 2.0 |
| | | : columnCenters[column]; |
| | | double x = centerX - width / 2.0; |
| | | double y = top + rowIndex * rowStep; |
| | | drawLabelWithHalo(graphics, item.getText(), |
| | | new Rectangle2D.Double(x, y, width, height), metrics, item.getColor()); |
| | | occupied.add(new Rectangle2D.Double(x - 2.0, y - 2.0, |
| | | width + 4.0, height + 4.0)); |
| | | } |
| | | |
| | | } |
| | | } |
| | | |
| | |
| | | } |
| | | |
| | | private static class HorizontalStackCallout { |
| | | private final int seriesIndex; |
| | | private final double startX; |
| | | private final double endX; |
| | | private final double y; |
| | | private final String text; |
| | | private final Color color; |
| | | |
| | | private HorizontalStackCallout(double startX, double endX, double y, |
| | | private HorizontalStackCallout(int seriesIndex, double startX, double endX, double y, |
| | | String text, Color color) { |
| | | this.seriesIndex = seriesIndex; |
| | | this.startX = startX; |
| | | this.endX = endX; |
| | | this.y = y; |
| | |
| | | this.color = color; |
| | | } |
| | | |
| | | private int getSeriesIndex() { return seriesIndex; } |
| | | private double getStartX() { return startX; } |
| | | private double getEndX() { return endX; } |
| | | private double getY() { return y; } |
| | |
| | | return total; |
| | | } |
| | | |
| | | static int scaleLineChartHeight(int height) { |
| | | return (int) Math.round(height * LINE_CHART_HEIGHT_SCALE); |
| | | } |
| | | |
| | | static long scaleLineChartHeight(long height) { |
| | | return Math.round(height * LINE_CHART_HEIGHT_SCALE); |
| | | } |
| | | |
| | | static BigDecimal displayValue(BigDecimal value, boolean percentItemLabels, int valueDecimals) { |
| | | if (value == null) return null; |
| | | int scale = percentItemLabels ? PERCENT_ITEM_LABEL_DECIMALS + 2 : valueDecimals; |
| | | return value.setScale(scale, RoundingMode.HALF_UP); |
| | | } |
| | | |
| | | private String formatLineLabel(Number value, boolean percentItemLabels, int valueDecimals) { |
| | | if (percentItemLabels) { |
| | | NumberFormat formatter = NumberFormat.getPercentInstance(); |
| | | formatter.setMaximumFractionDigits(1); |
| | | formatter.setMaximumFractionDigits(PERCENT_ITEM_LABEL_DECIMALS); |
| | | return formatter.format(value.doubleValue()); |
| | | } |
| | | DecimalFormat formatter = new DecimalFormat("#,##0"); |