How to implement a Box Plot in the Report Module?

Hi everyone,

I am looking for a way to generate a Box Plot within the Ignition Report Module.

As shown in the attached image , we need to visualize power distribution (kW) across multiple Racks and have a requirement to include this exact visualization in a scheduled PDF report.

My Questions:

  • Has anyone successfully created a Box Plot in the Report Module?

  • If you've done this before, would you mind sharing a snippet or a general approach on how to structure the data for the chart?

  • Are there any reputable third-party modules that add advanced statistical charting like Box Plots to the Report Module?

Any advice, screenshots, or sample would be greatly appreciated!

Start with a bar chart and use the BoxAndWhiskerRenderer.

You might need to tweak the chart with the configureChart() scripting method.

Hi @pturmel,

I'm trying to implement a Box Plot in the Report Module following your suggestion to use a Bar Chart with a BoxAndWhiskerRenderer.

The chart renders the axes correctly, but the plot area remains empty (no boxes). I've attempted to force the dataset binding within configureChart(), but it hasn't resolved the issue.

Could you please check if my script logic for the BoxAndWhiskerCategoryDataset or the renderer hand-off is incorrect?


1. Script Data Source (updateData)

Python

def updateData(data, sample):
    # Import specialized dataset for Box Plots
    from org.jfree.data.statistics import DefaultBoxAndWhiskerCategoryDataset
    
    # Initialize the dataset
    ds = DefaultBoxAndWhiskerCategoryDataset()
    
    # Static debug data: [Min, Q1, Median, Q3, Max] represented by a list
    test_values = [5, 7, 8, 10, 12, 15, 18, 20, 22, 25]
    
    # Add data: (List of values, Series, Category)
    ds.add(test_values, "Power", "Rack 01")
    
    # Pass to the report data map
    data['debugData'] = ds

2. Bar Chart Extension Function (configureChart)

Python

def configureChart(self, chart):
    from org.jfree.chart.renderer.category import BoxAndWhiskerRenderer
    import java.awt.Color
    
    plot = chart.getCategoryPlot()
    
    # Retrieve the dataset from the report data map
    ds = self.data.get("debugData")
    
    if ds:
        # Explicitly set the dataset to the plot
        plot.setDataset(ds)
    
    # Initialize and configure the BoxAndWhisker renderer
    renderer = BoxAndWhiskerRenderer()
    renderer.setFillBox(True)
    renderer.setSeriesPaint(0, java.awt.Color.WHITE)
    renderer.setArtifactPaint(java.awt.Color.BLACK)
    
    # Apply the renderer to the plot
    plot.setRenderer(renderer)

All of the use of JFreeChart objects has to be in the configureChart method. Construct the BoxAndWhiskerDataset there.

In the report's scripted data, you have to use normal Ignition datasets.

In configureChart, consider using BoxAndWhiskerItem to add to the dataset, so you don't have to supply a list of raw data.

3 Likes

PS: It's going to take such careful LLM prompting to get this right that it will probably be easier to just write the code yourself. The same term, "dataset" is used by both JFreeChart and Ignition, but they are not mutually compatible classes.

2 Likes

Thank you Paul Griffith and pturmel for your insightful suggestions. Following your advice, I have successfully implemented a simulated Box and Whisker plot using JFreeChart within Ignition.

With some assistance from Gemini to refine the statistical logic and rendering, I’ve managed to generate a consistent 56-rack distribution with fixed color palettes and optimized axis layouts. While the simulation is working well, I recognize there is still room for improvement in terms of data binding efficiency and script modularity.

I’ve included my current scripts below for reference. I would appreciate any further feedback on how to make this more robust for a production environment.

def updateData(data, sample):

import random
headers = ["Rack", "Value"]
rows = []

# Iterate through 56 Racks
for i in range(1, 57):
    rackName = "Rack %02d" % i
    
    # Set a random base power (10 - 30 kW)
    base_power = random.uniform(10, 30)
    
    # Set random volatility (Standard deviation between 1.0 and 5.0)
    # This ensures some racks have tight data (small box) while others are dispersed (large box)
    variation = random.uniform(1.0, 5.0)
    
    # Generate 20 data points per Rack
    for _ in range(20):
        val = random.gauss(base_power, variation)
        # Ensure no negative power values and round to two decimal places
        val = max(0, round(val, 2))
        rows.append([rackName, val])
        
# Store the result in the 'data' dictionary for the Chart Script to call
data['rawIgnitionData'] = system.dataset.toDataSet(headers, rows)

def configureChart(data, chart):

from org.jfree.data.statistics import DefaultBoxAndWhiskerCategoryDataset
from org.jfree.chart.renderer.category import BoxAndWhiskerRenderer
from org.jfree.chart.axis import CategoryLabelPositions
from org.jfree.ui import RectangleInsets
import java.awt.Color
import java.awt.BasicStroke
import random

jfreeDS = DefaultBoxAndWhiskerCategoryDataset()

# --- 1. Data Generation ---
for i in range(1, 57):
    rackName = "Rack %02d" % i
    base = random.uniform(10.0, 30.0)
    box_half = random.uniform(1.0, 5.0)
    q1, q3 = base - box_half, base + box_half
    med = base + random.uniform(-1.0, 1.0)
    ext = random.uniform(2.0, 12.0)
    low_limit = max(0.0, q1 - ext) 
    high_limit = q3 + ext
    # Construct statistical points: [Min, Q1, Median, Q3, Max]
    points = [low_limit, low_limit, q1, med, q3, high_limit, high_limit]
    jfreeDS.add(points, "Power", rackName)

plot = chart.getCategoryPlot()
plot.setDataset(jfreeDS)

# --- 2. Renderer Settings (Custom Colorful Boxes - Fixed Palette) ---
class CustomRenderer(BoxAndWhiskerRenderer):
    def __init__(self):
        self.colors = []
        # Use a local Random instance with a fixed seed (42) 
        # to ensure the color list remains consistent across refreshes.
        color_gen = random.Random(42)
        for _ in range(56):
            self.colors.append(java.awt.Color(color_gen.randint(40, 180), 
                                            color_gen.randint(40, 180), 
                                            color_gen.randint(40, 180)))
                                            
    def getItemPaint(self, row, column):
        # Assign color based on column index; e.g., Rack 01 (index 0) 
        # will always use the first color in the list.
        return self.colors[column % len(self.colors)]

renderer = CustomRenderer()
renderer.setFillBox(True)
renderer.setSeriesOutlinePaint(0, java.awt.Color.BLACK) 
renderer.setSeriesOutlineStroke(0, java.awt.BasicStroke(1.2))
renderer.setArtifactPaint(java.awt.Color.WHITE) # Sets the color of the median line

try:
    renderer.setMeanVisible(False)     
    renderer.setWhiskerWidth(0.8)      
    renderer.setUseOutlinePaintForWhiskers(True) 
except:
    # Graceful exit for older JFreeChart versions lacking these methods
    pass
    
plot.setRenderer(renderer)

# --- 3. X-Axis Optimization (Domain Axis) ---
domainAxis = plot.getDomainAxis()
domainAxis.setLowerMargin(0.01) 
domainAxis.setUpperMargin(0.01) 
domainAxis.setCategoryMargin(0.05) 
domainAxis.setCategoryLabelPositions(CategoryLabelPositions.DOWN_90)
domainAxis.setTickLabelFont(domainAxis.getTickLabelFont().deriveFont(7.0))

# --- 4. Y-Axis & Margin Optimization (Compressing left side space) ---
rangeAxis = plot.getRangeAxis()
rangeAxis.setRange(0.0, 45.0)
rangeAxis.setLabel("kW")

rangeAxis.setLabelFont(rangeAxis.getLabelFont().deriveFont(9.0))
rangeAxis.setTickLabelFont(rangeAxis.getTickLabelFont().deriveFont(9.0))
rangeAxis.setLabelInsets(RectangleInsets(0, 0, 0, 0)) 

chart.removeLegend()

# Set Plot Insets: (Top, Left, Bottom, Right)
plot.setInsets(RectangleInsets(10, 35, 60, 10))

Consider moving any code that needs an import to a project library script, and place all imports outside any function def.

I noticed that you aren't using the BoxAndWhiskerItem I recommended. Supplying a list to the .add() method is not the same.

1 Like