UDT Member Name on Custom Trend

I have a UDT that goes with a simple scale with parameters and alarms AOI inside our AB PLC. Members like

rawIn
rawMin
rawMax
scaledMin
scaledMax
scaledOut

alarmHighEN
alarmHighSP
alarmHighPRE
alarmHighIND

alarmLowEN
alarmLowSP
alarmLowPRE
alarmLowIND

In the UDT definition I have tag history configured for the scaledOut member. This works fine if I pre-configure the pen on the chart, but if I want to use a "custom" trend with the Tag Browse Tree and an Easy Chart when I drag and drop multiple resultants from different UDT instances they all have the same "scaledOut" tag name on the trend.

Is there a way to change the text displayed for the pen on a custom trend?

If not, this seems like an oversight where an extra (optional) tag history property could've been used. A "Pen Display Name" if you will. If left blank, it uses the tag name. If filled it, it uses whatever is entered (bindable property).

You'll have to implement your own tagsDropped event handler, and generate the new row(s) for the tagPens property yourself, with your preferred naming convention.

And what might that look like - assuming that a tag's UDT parent has a parameter that I want to show?

*edit: custom script solution below

Alternatively, could I use a property change script on the tagPens dataset to change the name? I'm thinking I can strip the tag path from each tagPen path, read all of the names of the tags and create the replacement dataset that way.

It's usually a bad idea to write back to a property that just triggered a change event--infinite recursion.

I don't have an implementation of onTagsDropped handy at the moment. Try some of these topics:

https://forum.inductiveautomation.com/search?q=onTagsDropped%20order%3Alatest

What if I were to use a check on the number of rows in the dataset before doing any property updates?

proertyChange script:

if event.propertyName == 'tagPens':
	if event.newValue.getRowCount() > event.oldValue.getRowCount():
		...

There are times when you dont want to show the full pen name, for example in a popup for the device, where you just want to see the tag's name and not the context, so it could never be as simple as you request here

1 Like

You can probably make some fragile script work with some anti-recursion check. Which will break when someone looks cross-eyed at it. Just say no to hacks.

It would be nice if the tagsDropped function at least had the script for the default behavior in it so I can see how to format everything and such. Easy enough to just delete or comment out if not needed.

Some examples in this thread.

I have one, I’m afk at the moment but I can post it in the morning.

Thanks. I was able to work one out on my own.

def customChartTagsDropped(self, paths):
	#headers - for reference
	#headers = [	'NAME',					'TAG_PATH',				'AGGREGATION_MODE',	'AXIS',			'SUBPLOT',
	#				'ENABLED',				'COLOR',				'DASH_PATTERN',		'RENDER_STYLE',	'LINE_WEIGHT',
	#				'SHAPE',				'FILL_SHAPE',			'LABELS',			'GROUP_NAME',	'DIGITAL',
	#				'OVERRIDE_AUTOCOLOR',	'HIDDEN',				'USER_SELECTABLE',	'SORT_ORDER',	'USER_REMOVABLE'
	#			]
				
	#default row - configure as desired for default trend appearance
	defaultRow = [		'Name',				'[Provider]Path',		'MinMax',			'Default Axis',	1,
						True,				'color(255,0,0,255)',	'',					0,				1,
						0,					True,					False,				'',				False,
						False,				False,					True,				'',				True
				 ]
	
	#initialize empty tagPaths list
	tagPaths = []
	tagNamePaths = []
	
	#for each path in the list of dropped tags
	for path in paths:
	
		#each tag provider is formatted as "area_xx" find the first occurance of the tag provider name
		pathStartIndex = path.find("area_")
		
		#check for tag in the default provider if the "area" provider is not found
		if pathStartIndex == -1:
			pathStartIndex = path.find("default")
		
		#make sure provider was found
		if pathStartIndex != -1:
		
			#get the tag path from the proivder to the end and format to read the custom PenName property from the tag
			tagPath = "["+path[pathStartIndex:]+'.PenName'
			tagNamePath = "["+path[pathStartIndex:]+'.Name'
			
			#add the tagPath to the list
			tagPaths.append(tagPath)
			tagNamePaths.append(tagNamePath)
			
		#tag provider not found, append empty string to keep accurate parallel lists
		else:
			tagPaths.append('')
			
	penNames = system.tag.readBlocking(tagPaths)
	tagNames = system.tag.readBlocking(tagNamePaths)
	
	#for each pen name in the list of read pen names
	for count, penName in enumerate(penNames):
	
		#skip any tags where the provider was not found
		if tagPaths[count] != '':
		
			#create default row
			newRow = defaultRow
			
			#if PenName custom prop does not exist or is empty
			if penName.quality.isBad() or penName.value is None or str(penName.value) == '':
			
				#assign tag name row name
				newRow[0] = tagNames[count].value
				
			#good PenName found
			else:
			
				#assign PenName to row name
				newRow[0] = str(penName.value)
				
			#assign history path to row path
			newRow[1] = paths[count]
			
			#update tagPens with the new row
			self.tagPens = system.dataset.addRow(self.tagPens, self.tagPens.rowCount, newRow)

This script relies on the trended tag to have a custom PenName property that is used for the name. If it doesn't exist or is blank the tag name is used instead.

One way to improve this would be to create the PenName prop if it doesn't exist and prompt the user to fill it in, but most/all of the trended tags are part of a UDT so that could get hairy.

Awesome.

For posterity, this is a simple version of what we do (we add some other aggregates and display them in a table as well as randomly generate pen colors and manage subplots and multiple axis, but I've removed all of that for simplicity).

from com.inductiveautomation.ignition.common import QualifiedPathUtils

for path in paths:
	qp = QualifiedPathUtils.toPathFromHistoricalString(path)
	
	#This will get the actual tag name, to use as the name of the Pen.
	penName = qp.getLastPathComponent()
	
	#get the current Tag Pen Dataset to do some checks.
	tagPens = self.tagPens
	
	pens = tagPens.getColumnAsList("Name")
	penPaths = tagPens.getColumnAsList("Tag_Path")
	
	if not penName in pens and path not in penPaths:
		#build a row to add to the tagPens dataset
	
		#Tag Pen Dataset Columns
		#"NAME","TAG_PATH","AGGREGATION_MODE","AXIS","SUBPLOT","ENABLED","COLOR","DASH_PATTERN","RENDER_STYLE","LINE_WEIGHT","SHAPE","FILL_SHAPE","LABELS","GROUP_NAME","DIGITAL","OVERRIDE_AUTOCOLOR","HIDDEN","USER_SELECTABLE","SORT_ORDER","USER_REMOVABLE"
		
		tagPen = [penName,path,"MinMax","Default Axis",1,True,"","",1,1.0,0,True,'',0,False,False,False,None,True]
	
		self.tagPens = system.dataset.addRow(tagPens, tagPen)

For what it's worth, if you want to get the path component from a qualified path you can do it like this, so in your case to get the PenName property

penNames = [qv.value for qv in system.tag.readBlocking([QualifiedPathUtils.toPathFromHistoricalString(path).getPathComponent('tag') + '.PenName' for path in paths])
2 Likes

How can I check for bad quality? My

if penName.quality == 'Bad_NotFound' or str(penName.value) == '':

doesn't seem to be working. On a tag where I know the quality is bad (by printing it before this if statement) I get

penName.quality == 'Bad_NotFound' -> False
str(penName.value) == '' -> False

I can add or penName.value is None and that works, but my initial question still stands as well.

The quality element of a QualifiedValue is a rich object, not a plain string. It has various methods on it, including isGood, isBad, etc.
So penName.quality.isBad() would work in your if statement.

2 Likes