Table cell change to date and time picker

I want to be able to select time from the perspective table.
The table get data from a dataset.


it's setup as per the following post.

the table has been setup nicely.
But for the viewparams setup, how do I point this value to the value of this row at this column?
or do I need to write script to get the row index, then assign the value of this Value[row][column] to this param?

I searched the forum, but haven't found the answer yet.

Regards

Change the name of the view parameter to value. Do not configure it in columns, as it is automatic.

1 Like

Thank you for the reply.

I have now configured to value, now I can see the different time values of each row.

Then I changed params of the value so I can read and write, but somehow I cannot change values from the table. If I change directly from the dataset, the new values can be updated to the table.

  1. component params value setup.
  2. time picker value binding. bidirectional is already selected.
  3. the table configuration of the columns. I tried with or without the viewParams setting of the value tag, neither way works.

when changing the time by clicking the up/down arrow, 1 sec later, the value changed back.
do I need to setup any action trigger each time I change the value?

Thanks.

You need to message to the containing view so it can change the table data.

1 Like

There are several implicit parameters for the subviews. See:

https://forum.inductiveautomation.com/t/perspective-table-dynamic-column-viewparams-per-row/34657/2

or
https://www.docs.inductiveautomation.com/docs/8.1/appendix/components/perspective-components/perspective-display-palette/perspective-table/displaying-a-subview-in-a-table-row#:~:text=Summary%20of%20Subviews%E2%80%8B,a%20value%20under%20this%20object.

So your viewParams would not include the element ‘time’.

You don’t even have to add the implicit parameters in the columns viewParams configuration.

You DO have to declare them in the embedded view with the correct name or you won’t have a property value to reference them.

embedded_view.params: {

value: null, # The value of the row from the selected column arrives here. The param property name must be ‘value’.

rowData: null, # Include this if you need to refer to data from other columns.

rowIndex: 0, # Include this if you need the index into the full dataset.

row: 0 # Include this if you need the index into the filtered dataset.

}

Hopefully, that answers your question. Happy programming!

1 Like

Is the param bidirectional?

Thanks all for the comments.

Here's the working solution.

  1. setup a time picker component.
  2. configure these parameters.

    column, columnIndex, row, rowIndex and value, are the default params passed from the table to the component. As I have two time picker, one for start time, one for end time. so I create a new tag "tag_name" to record which time it is.
  3. the time picker value is bound to the view.params.value.

    the change script of the value binding is:
def valueChanged(self, previousValue, currentValue, origin, missedEvents):
		value = currentValue.value
		if value is not None:
			if 'Binding' not in str(origin):
				msg = 'UpdateRow'
				rowIndex = self.view.params.rowIndex
				tag_name = self.view.params.tag_name
				payload =	{
							'value'	: value,
							'rowIndex': rowIndex,
							'tag_name': tag_name
							}
				system.perspective.sendMessage(msg, payload)

once configured, whenever the time picker value is changes, it will send a message.
4. this is the table configuration of the time picker.
Processing: image.png…
5. this is the message handler script of the table.

def onMessageReceived(self, payload):

	value = payload['value']
	rowIndex = int(payload['rowIndex'])
	tag_name = str(payload['tag_name'])
	tagPath = "[default]Site_Control/ds_csp_1"
	dataset = system.tag.readBlocking(tagPath)[0].value
	system.tag.write(tagPath, system.dataset.setValue(dataset, rowIndex, tag_name, value))

In summary, the basic data flow is:

  1. operator change time picker value.
  2. the following values are sent from the table component to the embedded time picker component.
    column, columnIndex, row, rowIndex and value
  3. the time picker component detects the value change, then it executes the message script.
  4. the table received the message handler, it executes the logic to write to the table binding tag
  5. the updated tag is bound to the table, then the table is refreshed with the new value.

as Phil mentioned, you need to create message to send back the updated value.

The following script converts pyDataset to dataset.
I found it handy sometimes.

def pyds_to_ds(value):
	   headers = system.dataset.getColumnHeaders(value)
	   newData=[]
	   for rowIndex in range(value.rowCount):
	       	row = [value.getValueAt(rowIndex,col) for col in range(value.columnCount)]
	       	newData.append(row)
	   return system.dataset.toDataSet(headers,newData)

This solution works well for me when I use Dataset, but when I convert it to PyDataSet (to change cell background to highlight exceeded limits), then the date value is not sent into the embedded view. Does this automatic sending of a column parameter named "value" work for Dataset only but not for PyDataSet?
I also tried to create the parameter named "value" explicitly.

... when I convert it to PyDataSet (to change cell background to highlight exceeded limits) ...

I've only done that by building a JSON dictionary as used in the sample data loaded by default with the Perspective Table component. The manual offers alternatives.
Styling Rows Based on Values | Ignition User Manual.

I have fixed the issue by adding a condition to skip the date column. It now stays scalar so the automatic passing of the "value" parameter into the embedded view works well.
@Transistor , thank you for your comment. It has lead my research into the right direction.
Here's the resulting code of my Transform script:

def transform(self, value, quality, timestamp):
    
    HI_LIMIT = 5.0
    
    for row in value: 								# Iterate through each row in the data
        for column, cell_value in row.items():      # Iterate through each cell in the row
            if column != "SampleDate":				# Skip SampleDate column not to break the date picker in the embedded view
	            
	            if cell_value > HI_LIMIT:
	                row[column] = {
	                    "value": cell_value,
	                    "style": {
	                        "backgroundColor": "#FFAAAA"
	                    }
	                }                
                	
    return value

Nobody has ever accused me of being an expert coder but I did pick up some useful tips on CodeAesthetic's Why You Shouldn't Nest Your Code. He's using C in the video but the same principles will apply to your Python code.

Your code is four levels deep by the time you get to row[column] = {. I've refactored it below, refactoring by "inversion" with early continue and now it's only two levels deep. I think it's a lot easier to follow.

Indeed, the code looks better without the excessive nesting. Thank you for your suggestion, @Transistor. Only I think we need to use continue instead of return to skip the current cycle and proceed with the next iteration. Also, these two conditions could be combined into one:

if (column = "SampleDate") or (cell_value <= HI_LIMIT):
    continue

In my resulting code it cannot be done, though, because the structure changed a bit:

def transform(self, value, quality, timestamp):
    # Fetch limits from the database using a named query
    limits_query = system.db.runNamedQuery("Select_all_limits")
    
    # Extract low and high limits from the query result
    low_limits  = limits_query[0]
    high_limits = limits_query[1]
    
    for row in value:  # Iterate through each row in the data
        for column, cell_value in row.items():  # Iterate through each cell in the row
            if (column in ("SampleDate", "id")) or cell_value == None:  # Skip SampleDate column not to break the date picker in the embedded view
            	continue
            try:
                numeric_value = float(cell_value)  # Convert cell value to a float
                
                # Get the low and high limits for the current column
                low_limit  = float( low_limits[column])
                high_limit = float(high_limits[column])
                
                # Check if the value is outside the limits and set the background color accordingly
                if (numeric_value >= low_limit) and (numeric_value <= high_limit):
                	continue 
                row[column] = {
                    "value": cell_value,
                    "style": {
                        "backgroundColor": "#FFAAAA"  # Red background for out-of-limit values
                    }
                }
            except ValueError:
                pass  # if conversion to float fails
    
    return value

You're right. I'll edit my post!