Configuring Ignition Chart Y-Axis Bounds from Dynamic Values

David Krause10 min read
HMI / SCADAOther ManufacturerTroubleshooting
Licensed PE Working through this on a live machine? A Maine-licensed engineer can take it from here — included with IMD hardware, by the hour for everything else. Book an engineer

Mechanism: why configureChart edits never reach the screen

The Vision Classic Chart wraps a JFreeChart object. The term range axis here means the Y-axis object reached through chart.plot.rangeAxis, a JFreeChart NumberAxis. The configureChart extension function is a hook the component calls while it builds or rebuilds that chart object. The component does not call it when your data changes, when a custom property such as graph_1_min changes, or when a checkbox elsewhere on the window is toggled.

The failing installation used this script inside configureChart:

def configureChart(self, chart):
    if self.parent.getComponent('Group').getComponent('CheckBox').selected == True:
        chart.plot.rangeAxis.setAutoRange(False)
        chart.plot.rangeAxis.setLowerBound(float(self.parent.parent.graph_1_min))
        chart.plot.rangeAxis.setUpperBound(float(self.parent.parent.graph_1_max))
    else:
        chart.plot.rangeAxis.setAutoRange(True)

The Chart Customizer showed the new bounds, so the axis object itself accepted the values. The plot on screen stayed on the old scale until the operator clicked in the chart and drag-selected a region, which forced a new render. Refreshing the component did not help. That pattern means the logic ran at the wrong time and the checkbox toggle and min/max property changes never re-executed it; nothing is wrong with the axis calls themselves.

The fix belongs in an event that fires when the input changes: a propertyChange handler on the component that owns the changing value. Two designs work:

  • Event-driven: push new bounds into chart.plot.rangeAxis from propertyChange handlers. Simple, confirmed working for the checkbox case.
  • Pull-driven: replace the reference markers and the range axis with Jython subclasses that read the custom properties every time JFreeChart asks for a value. More code, but no handler can be missed.

Symptoms versus causes

Symptom Cause Resolving action
Customizer shows new bounds, plot does not rescale until you click or drag in the chart Axis code lives in configureChart, which does not run on checkbox or custom-property changes Move the code to a propertyChange handler (Check 3)
Bounds apply on checkbox toggle, but do not follow later changes to graph_1_min/graph_1_max Only the checkbox has a handler Add a handler on the component that owns the custom properties (Check 4)
Script error mentioning float() or a missing attribute Property path resolves to the wrong container, or the property is null at window open Verify the path and guard against null (Check 1, Check 2)
Fixed scale is lost after the operator zooms and resets the zoom JFreeChart zoom reset re-enables auto-range on the axis Reapply bounds, or disable range zoom (Check 5)
Upper bound lands one unit above lower bound after a large range jump Separate setLowerBound call ran while the old upper bound was below the new lower bound Set both limits in one setRange call

Check 1: resolve where the bound values live

Read the component tree before writing any handler. The two scripts reach the custom properties through different paths:

  • In configureChart, self is the chart, so self.parent.parent.graph_1_min is two containers above the chart.
  • In the checkbox handler, the checkbox sits inside Group, so event.source.parent.parent is the container that holds both Group and Chart, which is one level above the chart.

Those resolve to different containers unless the tree differs from the names shown. Open the Project Browser, select the component that actually carries graph_1_min and graph_1_max, and count the levels from the checkbox and from the chart.

  • Properties on the container holding Chart and Group: use event.source.parent.parent from the checkbox. Go to Check 2.
  • Properties on the root container or higher: add one .parent per level, or read them from the root container reference. Go to Check 2.

Hard-coded .parent chains break when a component is moved into or out of a group. Keep the custom properties on the same container that holds the chart where the layout allows it.

Check 2: read the property values at the moment the handler fires

Print both values from the handler before touching the axis:

print event.source.parent.parent.graph_1_min, event.source.parent.parent.graph_1_max

Open the Designer console (or the client console at runtime) and toggle the checkbox.

  • Two numbers, min less than max: go to Check 3.
  • None on either value: the binding feeding the property has not resolved yet (common at window open, or with a bad tag quality). Guard with a null check and skip the axis update. Then go to Check 3.
  • Min greater than or equal to max: the source data is inverted or both limits are equal. JFreeChart requires lower strictly below upper. Fix the source, or fall back to auto-range.
  • AttributeError: the path is wrong. Return to Check 1.

Check 3: drive the axis from the checkbox propertyChange event

This is the resolving branch for the checkbox case, and it was confirmed working. Place the script on the checkbox's propertyChange event handler and remove the axis logic from configureChart. Filter on propertyName, because propertyChange fires for every property on the component, including ones changed by the component itself during startup.

if event.propertyName == 'selected':
    chart = event.source.parent.parent.getComponent('Chart').chart
    if event.newValue:
        chart.plot.rangeAxis.setAutoRange(False)
        chart.plot.rangeAxis.setLowerBound(float(event.source.parent.parent.graph_1_min))
        chart.plot.rangeAxis.setUpperBound(float(event.source.parent.parent.graph_1_max))
    else:
        chart.plot.rangeAxis.setAutoRange(True)

A hardened version sets both limits in a single call. ValueAxis.setRange(lower, upper) applies the pair atomically and switches auto-range off as a side effect. The separate setters are order-sensitive: setLowerBound checks the new value against the current upper bound, and when the new lower is above it, JFreeChart substitutes lower + 1 as the upper. The following setUpperBound normally corrects that, but only if it runs.

if event.propertyName == 'selected':
    container = event.source.parent.parent
    axis = container.getComponent('Chart').chart.plot.rangeAxis
    lo = container.graph_1_min
    hi = container.graph_1_max
    if event.newValue and lo is not None and hi is not None and float(lo) < float(hi):
        axis.setRange(float(lo), float(hi))
    else:
        axis.setAutoRange(True)

Any change made to a live JFreeChart axis fires an axis change event, which the chart panel turns into a repaint. The handler therefore takes effect immediately, with no refresh call.

  • Min/max limits are fixed for the life of the window: this is the complete fix. Go to Verification.
  • Min/max limits change while the checkbox stays selected: go to Check 4.

Check 4: follow min/max changes while the checkbox stays selected

A checkbox handler only runs when selected changes. If graph_1_min or graph_1_max is bound to a tag or a query that updates, add a second propertyChange handler on the component that owns those custom properties. Custom properties fire propertyChange on their owning component with propertyName equal to the property name.

# propertyChange on the container that owns graph_1_min / graph_1_max
if event.propertyName in ('graph_1_min', 'graph_1_max'):
    src = event.source
    cb = src.getComponent('Group').getComponent('CheckBox')
    axis = src.getComponent('Chart').chart.plot.rangeAxis
    lo = src.graph_1_min
    hi = src.graph_1_max
    if cb.selected and lo is not None and hi is not None and float(lo) < float(hi):
        axis.setRange(float(lo), float(hi))

Adjust the getComponent paths to the tree found in Check 1. Keep the range logic in one project library function called from both handlers so the checkbox and the property handler never apply different rules.

When many charts, parameters (current, voltage, and so on), or limit lines need the same treatment, the handler count grows fast. Go to Check 6 for the pull-driven design.

Check 5: operator zoom and the fixed range

A drag-select on the Classic Chart zooms the plot by calling setRange on the axis. That is why dragging revealed the stale bounds in the original fault. The reverse gesture, the zoom reset, calls the axis resize with a zero factor, and JFreeChart treats that as "return to auto-range". After a zoom reset the axis ignores the fixed limits until your code applies them again.

  • Operators need zoom: accept that zoom reset returns to auto-range, and tell operators to toggle the checkbox to reapply fixed limits. Alternatively, use the pull-driven axis in Check 6, which ignores zoom state.
  • Operators do not need zoom: disable range zooming on the chart (the component's zoom settings, or setRangeZoomable(False) on the underlying chart panel if you script it). The fixed range then persists.

Check 6: pull-driven markers and axis for fully dynamic limits

The alternative design turns the direction around. Instead of pushing new values when something changes, give JFreeChart objects that compute their values on demand. The renderer asks a ValueMarker for its value on every repaint, so a subclass that calls back into your custom properties always draws the current Min, Max, and Target lines.

# project library script, e.g. chart_util
from org.jfree.chart.plot import ValueMarker
from java.awt import Color, BasicStroke

class DynValueMarker(ValueMarker):
    # callable returns the range value to mark
    def __init__(self, callable, clr, stroke):
        super(DynValueMarker, self).__init__(0.0, clr, stroke)
        self.callable = callable
    def getValue(self):
        return self.callable()

Attach the markers once, in configureChart, which is the correct place for work that only has to happen when the chart object is built:

def configureChart(self, chart):
    from java.awt import Color, BasicStroke
    container = self.parent
    plot = chart.plot
    plot.clearRangeMarkers()
    stroke = BasicStroke(2.0)
    plot.addRangeMarker(chart_util.DynValueMarker(lambda: float(container.graph_1_min), Color.RED, stroke))
    plot.addRangeMarker(chart_util.DynValueMarker(lambda: float(container.graph_1_max), Color.RED, stroke))

Markers alone do not trigger a repaint when a property changes; they only report the right value when the chart next draws. The chart repaints on each new data update, so trended data supplies the refresh. For the Y-axis bounds, apply the same pattern: replace plot.rangeAxis with a NumberAxis subclass whose range accessor reads the custom properties and the checkbox state, and returns auto-range bounds when the checkbox is clear. Build and test that subclass against the JFreeChart version shipped with your Ignition install, because axis accessor behavior varies between JFreeChart releases. For Min/Max/Target lines that should disappear when the checkbox is clear, have the callable return a value outside the visible range, or add and remove the markers from the checkbox handler.

Choose the pull-driven design when limits come from tags that change during operation, when zoom must not defeat the fixed range, or when several charts share the logic. Otherwise the event-driven handler in Check 3 is less code and easier to maintain.

Procedure and verification for the event-driven fix

  1. Delete the range-axis logic from configureChart on the chart. Leave only one-time setup there (markers, renderer styling).
  2. Confirm the owner of graph_1_min and graph_1_max in the Project Browser and write down the relative path from the checkbox and from that owner to the chart.
  3. Add the hardened selected handler from Check 3 to the checkbox propertyChange event, using the confirmed paths.
  4. If the limits are bound to live data, add the Check 4 handler to the owning container.
  5. Decide the zoom policy from Check 5 and configure the chart accordingly.
  6. Save, launch a client (or enter Preview mode), and run the checks below.

Verification checks:

  1. Check 1: select the checkbox without touching the chart. Expect the Y-axis to rescale immediately to graph_1_min..graph_1_max, with trend values outside that band clipped at the plot edge.
  2. Check 2: clear the checkbox. Expect the Y-axis to return to auto-range and every trend point, including the out-of-limit ones, to be visible.
  3. Check 3: with the checkbox selected, write a new value to the tag or property behind graph_1_max. Expect the upper limit to move within one update, with no click in the chart (only with the Check 4 handler in place).
  4. Check 4: set a new minimum above the old maximum and a new maximum above that. Expect the axis to show exactly the new pair, not lower + 1.
  5. Check 5: write a null or an inverted pair (min above max). Expect no script error in the console and the axis to hold its previous range or fall back to auto-range.
  6. Check 6: close and reopen the window with the checkbox saved as selected. Expect the fixed range to apply at open. If it does not, the selected event did not fire at startup; call the shared range function once from the window's internalFrameOpened event.

FAQ

Does configureChart run when a custom property or tag value changes on an Ignition Classic Chart?

No. configureChart runs when the chart object is built, not on data or custom-property changes, so bounds set there appear only after something else forces a redraw. Put runtime axis changes in a propertyChange handler instead.

Can I force the chart to repaint instead of moving the script?

Refreshing the component did not apply the bounds in the failing setup, because the logic that sets them never re-ran. Move the chart.plot.rangeAxis calls into the event that changes the input; each axis change then triggers its own repaint.

Can I set the Y-axis min and max in one call?

Yes. chart.plot.rangeAxis.setRange(lower, upper) applies both limits atomically and disables auto-range, which avoids the lower + 1 substitution JFreeChart makes when setLowerBound exceeds the current upper bound.

Does zooming on the Classic Chart cancel my fixed Y-axis range?

Drag-zoom sets a new range on the axis, and zoom reset returns the axis to auto-range, so the fixed limits are lost until your script reapplies them. Disable range zoom or use a custom axis that reads the limits on demand.

Can I show Min, Max and Target lines that follow live tag values?

Yes. Subclass org.jfree.chart.plot.ValueMarker, override getValue() to call a function that reads your custom property, and add the markers once with plot.addRangeMarker() in configureChart. The lines take the current value on every repaint.

Back to blog