| | |
| | | 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; |
| | |
| | | } |
| | | |
| | | public byte[] lineChart(String yAxisLabel, List<ChartSeries> series, boolean percentAxis) { |
| | | return lineChart(yAxisLabel, series, percentAxis, 1100, 590); |
| | | return lineChart(yAxisLabel, series, percentAxis, DAY_LABELS, 2, 1100, 590); |
| | | } |
| | | |
| | | public byte[] lineChart(String yAxisLabel, List<ChartSeries> series, boolean percentAxis, |
| | | int width, int height) { |
| | | return lineChart(yAxisLabel, series, percentAxis, 2, width, height); |
| | | return lineChart(yAxisLabel, series, percentAxis, DAY_LABELS, 2, width, height); |
| | | } |
| | | |
| | | public byte[] lineChart(String yAxisLabel, List<ChartSeries> series, boolean percentAxis, |
| | | int valueDecimals, int width, int height) { |
| | | return lineChart(yAxisLabel, series, percentAxis, DAY_LABELS, valueDecimals, width, height); |
| | | } |
| | | |
| | | public byte[] lineChart(String yAxisLabel, List<ChartSeries> series, boolean percentAxis, |
| | | List<String> dayLabels, int valueDecimals, int width, int height) { |
| | | XYSeriesCollection dataset = new XYSeriesCollection(); |
| | | for (int seriesIndex = 0; seriesIndex < series.size(); seriesIndex++) { |
| | | ChartSeries item = series.get(seriesIndex); |
| | |
| | | plot.setRangeGridlinesVisible(false); |
| | | plot.setDomainTickBandPaint(null); |
| | | plot.setRangeTickBandPaint(null); |
| | | SymbolAxis domainAxis = new SymbolAxis(null, DAY_LABELS.toArray(new String[0])); |
| | | List<String> axisLabels = dayLabels == null || dayLabels.isEmpty() ? DAY_LABELS : dayLabels; |
| | | SymbolAxis domainAxis = new SymbolAxis(null, axisLabels.toArray(new String[0])); |
| | | domainAxis.setGridBandPaint(new Color(0, 0, 0, 0)); |
| | | domainAxis.setGridBandAlternatePaint(new Color(0, 0, 0, 0)); |
| | | domainAxis.setLowerMargin(0.04); |
| | |
| | | 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; } |