The setup: a Perspective custom property calls an API and returns a nested object. Other custom properties read from it. Operators need to see the defect structure the way it looks in the Designer property editor, with expandable nodes. The defect list changes per coil. Some coils have no defects, some have several, and new defect names can arrive from the API at any time. Here is a typical defect record:
Defects
Wr1_LiftIsOff_Error
Comment: WR1-Bottom Lift roll is not enabled
Code: WR1_LiftIsOff_Error
EndingMeterMark: 436.46814
Perspective has no component that binds directly to an arbitrary object and renders the property-editor dropdowns. The Tree component comes closest, but it needs its own data shape. The fix that works is to regenerate the Tree's items array from the API object every time the object changes.
Skip the JSON Text Area Dump
The fastest thing to try is to encode the custom property as a JSON string and bind it to a Text Area or Label. It works in about two minutes. You bind the text property to the custom property and run it through system.util.jsonEncode in a transform.
It fails for operators:
- You get braces, quotes, and commas instead of named nodes. Nothing collapses.
- A long defect list pushes the useful lines off the screen.
- Operators on the floor read it slowly, and they will stop reading it.
Keep the JSON dump as a commissioning and debug view. Do not ship it as the operator screen.
Don't Hand-Build a Static Tree and Toggle Visibility
The second attempt is usually to build the Tree items by hand in the Designer, one node per known defect, then hide the nodes that don't apply to the current coil. That approach runs into three problems:
-
No per-item visibility on Tree items. Each Tree item is a plain object with
label,expanded,data, anditems. There is no hide flag to bind against, so hiding means deleting the node anyway. - Unknown defects never appear. A defect name the API adds next month has no node, so it silently goes missing.
-
Stale leaves. Values like
EndingMeterMarkchange per coil. Hand-built labels have to be bound one by one, and those bindings break when the structure shifts.
If you find yourself writing visibility logic per defect, stop. You are rebuilding the list by hand. Let a script do that instead.
Hold Off on a Custom Component
A dynamic list with unpredictable members usually pushes people toward building something from scratch, such as a custom module or a stack of nested embedded views. You don't need to. The Tree already renders any depth of nesting, expands and collapses nodes, and supports custom icons. What it lacks is a converter from your API shape to its item shape. That converter is about 30 lines of Jython in a binding transform. Get it running with the Tree first. Only consider a custom build if you later need behavior the Tree cannot provide.
Understand Why the Tree Needs a Rebuilt Items Array
The Tree does not walk an arbitrary object. It walks an array of item objects, where each item carries its children in items. Your API object stores data as keys (Wr1_LiftIsOff_Error, Comment, Code) and values. The Tree stores the same information as label strings and child arrays. So the transform must turn keys into labels and nested maps into child arrays.
Rebuild the array from scratch on every API update. Do not patch the existing array. A full rebuild handles every case with the same code:
- Zero defects: an empty array, or a single "No defects" node.
- A defect that cleared: it is simply not generated.
- A brand-new defect name: it appears automatically.
Unused items never exist, so you have nothing to hide.
| Symptom | Cause | Correction |
|---|---|---|
| Object shows as one unreadable string | Object bound to a text component, or JSON-encoded | Bind the Tree props.items through a script transform |
| Tree is blank although the custom property has data | Items array missing label/items keys, or the transform raised an error |
Check the binding error overlay and the gateway logs, then fix the item shape |
| Old defects remain after the coil changes | Items patched or appended instead of regenerated | Return a new list from the transform on each update |
| New defect types never appear | Nodes hand-built for known defect names | Generate nodes by iterating the object's keys |
| Expanded nodes collapse on every refresh | Full rebuild resets expanded
|
Set expanded in the transform for the levels operators need open |
Rebuild the Tree in a Script Transform
- Drop a Tree component into the view.
- On the Tree's
props.items, add a Property binding that points at the custom property holding the API result. The pathview.custom.defectsbelow is an example; use your own. - Add a Script transform and paste in the converter below.
- Leave the other custom properties that read the API result alone. The Tree binding is a read-only consumer.
- Save, then watch the Tree in Preview mode while the API property updates.
def transform(self, value, quality, timestamp):
def isMap(v):
return hasattr(v, 'keys')
def isList(v):
return (hasattr(v, '__iter__') and not isMap(v)
and not isinstance(v, basestring))
def build(label, v, depth):
# Open the first two levels (Defects + each defect)
openNode = depth < 2
if isMap(v):
children = [build(k, v[k], depth + 1) for k in v.keys()]
return {'label': label, 'expanded': openNode,
'data': {}, 'items': children}
if isList(v):
children = [build('[%d]' % i, x, depth + 1)
for i, x in enumerate(v)]
return {'label': label, 'expanded': openNode,
'data': {}, 'items': children}
# Leaf: key and value on one line
return {'label': '%s: %s' % (label, v), 'expanded': False,
'data': v, 'items': []}
if value is None or not quality.isGood():
return [{'label': 'Defect data unavailable', 'expanded': False,
'data': {}, 'items': []}]
defects = value.get('Defects', value) if isMap(value) else value
if not defects:
return [{'label': 'No defects', 'expanded': False,
'data': {}, 'items': []}]
return [build('Defects', defects, 0)]
A few notes on the script:
- Perspective hands object properties to scripts as dict-like wrappers. The
hasattr(v, 'keys')test catches those as well as plain dicts. If a branch renders as a single leaf when it should expand, logtype(v)withsystem.util.getLoggerand adjustisMapto match. - Key order follows the object. If operators want alphabetical defects, wrap the key list in
sorted(v.keys()). - The
'Defects'lookup assumes the API nests defects under that key, as in the example structure. Drop it if your custom property already holds the defect map itself.
Shape the Labels for Operators
A literal conversion of the property-editor view puts every value one level below its key. For example:
Comment
WR1-Bottom Lift roll is not enabled
Code
WR1_LiftIsOff_Error
EndingMeterMark
436.46814
That is accurate but costs the operator two clicks per field. The leaf branch in the transform collapses each key and value into one node (Comment: WR1-Bottom Lift roll is not enabled). Push further if it helps readability:
- Label defects with their comment instead of the code, and keep the code as a child.
-
Round meter marks in the leaf label, for example
'%.1f' % vwhenvis a float. Keep the raw value indatafor any click handling. - Use icons to separate defect nodes from detail nodes. The Tree supports custom icons per item. Set them in the same transform, so every generated node gets the right one.
-
Use
datafor the payload, not the label. Any selection or click event on the Tree readsdata, so store the defect code or the full defect object there.
Use a Table or Flex Repeater for Fixed-Depth Data
The Tree is the right choice when nesting depth varies or you want the property-editor feel. If every defect always has the same fields (comment, code, meter mark), a flat layout usually reads faster on an HMI:
| Option | Best when | Transform output |
|---|---|---|
| Tree | Depth varies, or operators drill into detail | Nested item list (script above) |
| Table | Same fields on every defect; operators scan and sort | List of row dicts, one per defect |
| Flex Repeater | Each defect needs a styled card, color, or buttons | List of instance param dicts feeding an embedded view |
| Text Area with JSON | Engineering and debug only |
system.util.jsonEncode of the object |
All three structured options follow the same rule: regenerate the full list from the API object on every update. Never try to hide rows or instances that don't apply.
Verify Against Empty, Single, and Burst Defect Sets
-
Zero defects. Force the custom property to an empty object. The Tree must show
No defectsand nothing else. - One defect. Load a single-defect record like the example. Confirm the comment, code, and meter mark leaves render with correct values.
- Several defects, then fewer. Load a record with several defects, then one with a subset. The cleared defects must disappear completely. If any remain, something is appending instead of the transform returning a new list.
- Unknown defect name. Add a made-up defect key to a test record. It must appear with no Designer changes.
- Bad quality or null. Break the API call, for example with a bad endpoint on a test view. The Tree must show the unavailable message instead of an error overlay.
- Logs. Check the gateway logs for script errors from the transform while cycling through the cases above.
-
Refresh behavior. Let the API update several times while a node is expanded. Confirm the default
expandeddepth leaves the screen usable for operators after each rebuild.
FAQ
Can I bind a Perspective Tree directly to a custom property object?
Not usefully. The Tree needs an array of items with label, expanded, data, and items keys. Bind props.items to the custom property and convert the object in a Script transform.
Does the Perspective Tree support hiding individual items?
Tree items have no visibility property. Leave unwanted nodes out of the items array by regenerating it from current data on every update, instead of hiding them.
Can I keep nodes expanded after the API data refreshes?
A full rebuild resets each node to whatever expanded value the transform assigns. Set expanded to true for the levels operators need open, such as the Defects root and each defect.
Does converting the object to JSON in a Text Area work for operators?
It displays everything, but operators find it hard to read and nothing collapses. Keep the JSON view for commissioning and debugging, and use a Tree, Table, or Flex Repeater on operator screens.
When should I stop and contact Inductive Automation support?
Stop if the transform returns a correctly shaped list but the Tree still renders blank or stale data. Also stop if the transform works in the Designer but throws errors only on the gateway. Collect the gateway logs, the exact Ignition version, and a sample of the API payload, then open a case through Inductive Automation's official support channel.