Search code examples
jfreechartoptaplanner

How to adjust colors in the UI of OptaPlanner?


I am currently using the OptaPlanner's job schedule algorithm to create a certain planning. I want every execution mode used in the planning to be shown in a different color (instead of all different projects to be shown in different colors). Is it possible to implement this and if so, how? I have been searching through the code for a while now and have no idea how to do this.


Solution

  • This cannot be done easily with the Project Scheduling Swing application that's part of OptaPlanner project. It plots the data using JFreeChart and I couldn't find a simple way to associate metadata (like color) with the data that's being plotted.

    You can override YIntervalRenderer behavior to return color of your choice based on data item's row (seriesIndex) and column (item's index in the series) but you have to keep the mapping between execution mode and [row, column] yourself, which is cumbersome.

    Here's an example of modified ProjectJobSchedulingPanel that does the above:

    public class ProjectJobSchedulingPanel extends SolutionPanel<Schedule> {
    
        private static final Logger logger = LoggerFactory.getLogger(ProjectJobSchedulingPanel.class);
        private static final Paint[] PAINT_SEQUENCE = DefaultDrawingSupplier.DEFAULT_PAINT_SEQUENCE;
    
        public static final String LOGO_PATH = "/org/optaplanner/examples/projectjobscheduling/swingui/projectJobSchedulingLogo.png";
    
        public ProjectJobSchedulingPanel() {
            setLayout(new BorderLayout());
        }
    
        @Override
        public void resetPanel(Schedule schedule) {
            removeAll();
            ChartPanel chartPanel = new ChartPanel(createChart(schedule));
            add(chartPanel, BorderLayout.CENTER);
        }
    
        private JFreeChart createChart(Schedule schedule) {
            YIntervalSeriesCollection seriesCollection = new YIntervalSeriesCollection();
            Map<Project, YIntervalSeries> projectSeriesMap = new LinkedHashMap<>(
                    schedule.getProjectList().size());
            ExecutionMode[][] executionModeByRowAndColumn = new ExecutionMode[schedule.getProjectList().size()][schedule.getAllocationList().size()];
            YIntervalRenderer renderer = new YIntervalRenderer() {
                @Override
                public Paint getItemPaint(int row, int column) {
                    ExecutionMode executionMode = executionModeByRowAndColumn[row][column];
                    logger.info("getItemPaint: ExecutionMode [{},{}]: {}", row, column, executionMode);
                    return executionMode == null
                            ? TangoColorFactory.ALUMINIUM_5
                            : PAINT_SEQUENCE[(int) (executionMode.getId() % PAINT_SEQUENCE.length)];
                }
            };
            Map<Project, Integer> seriesIndexByProject = new HashMap<>();
            int maximumEndDate = 0;
            int seriesIndex = 0;
            for (Project project : schedule.getProjectList()) {
                YIntervalSeries projectSeries = new YIntervalSeries(project.getLabel());
                seriesCollection.addSeries(projectSeries);
                projectSeriesMap.put(project, projectSeries);
                renderer.setSeriesShape(seriesIndex, new Rectangle());
                renderer.setSeriesStroke(seriesIndex, new BasicStroke(3.0f));
                seriesIndexByProject.put(project, seriesIndex);
                seriesIndex++;
            }
            for (Allocation allocation : schedule.getAllocationList()) {
                int startDate = allocation.getStartDate();
                int endDate = allocation.getEndDate();
                YIntervalSeries projectSeries = projectSeriesMap.get(allocation.getProject());
                int column = projectSeries.getItemCount();
                executionModeByRowAndColumn[seriesIndexByProject.get(allocation.getProject())][column] = allocation.getExecutionMode();
                logger.info("ExecutionMode [{},{}] = {}", seriesIndexByProject.get(allocation.getProject()), column, allocation.getExecutionMode());
                projectSeries.add(allocation.getId(), (startDate + endDate) / 2.0,
                        startDate, endDate);
                maximumEndDate = Math.max(maximumEndDate, endDate);
            }
            NumberAxis domainAxis = new NumberAxis("Job");
            domainAxis.setStandardTickUnits(NumberAxis.createIntegerTickUnits());
            domainAxis.setRange(-0.5, schedule.getAllocationList().size() - 0.5);
            domainAxis.setInverted(true);
            NumberAxis rangeAxis = new NumberAxis("Day (start to end date)");
            rangeAxis.setRange(-0.5, maximumEndDate + 0.5);
            XYPlot plot = new XYPlot(seriesCollection, domainAxis, rangeAxis, renderer);
            plot.setOrientation(PlotOrientation.HORIZONTAL);
            // Uncomment this to use Tango color sequence instead of JFreeChart default sequence.
            // This results in color per project mode.
    //        DefaultDrawingSupplier drawingSupplier = new DefaultDrawingSupplier(
    //                TangoColorFactory.SEQUENCE_1,
    //                DefaultDrawingSupplier.DEFAULT_FILL_PAINT_SEQUENCE,
    //                DefaultDrawingSupplier.DEFAULT_OUTLINE_PAINT_SEQUENCE,
    //                DefaultDrawingSupplier.DEFAULT_STROKE_SEQUENCE,
    //                DefaultDrawingSupplier.DEFAULT_OUTLINE_STROKE_SEQUENCE,
    //                DefaultDrawingSupplier.DEFAULT_SHAPE_SEQUENCE);
    //        plot.setDrawingSupplier(drawingSupplier);
            return new JFreeChart("Project Job Scheduling", JFreeChart.DEFAULT_TITLE_FONT, plot, true);
        }
    }
    

    Result: enter image description here

    Another approach would be to implement JFreeChart interfaces and make custom Dataset and Renderer so that you could plot Allocations directly. Similar to the Gantt chart implementaion in JFreeChart.

    Or write your custom UI from the ground up. Depends op how much effort you're willing to put into it :)